0
\$\begingroup\$

I'm implementing an ECS and have run into a predicament.

Let's say my main loop is something like (super simplified, all code below is pseudo-code)

while (true) {
    // timing logic in here somewhere
    for (GameSystem system : systems) {
        for (GameEntity entity : entities) {
            system.update(entity);
        }
    }
}

Components need to have their state updated, but how exactly that state gets updated is kind of blurry.

Let's say I have an InputComponent base type. There can be lots of InputComponent types, keyboard, controller, etc. I (as the game engine) don't really care that you pressed the W key with the intent to move up, or that you pressed the Spacebar key to jump. That is outside the scope of my concern. What I do care about is the intent. I do care that you want to move up, or to jump, but don't care how that command gets generated. How that command gets generated is up to the InputComponent to decide. Otherwise, I would have to dirty up my system code with something like

class MovementSystem {

    void update(GameEntity entity) {
        if (entity.getSettings().isUsingKeyboardInput()) {
            if (entity.getKeyboardInput().isSKeyPressed()) {
                moveDown();
            }
            else if (entity.getKeyboardInput().isWKeyPressed()) {
                moveUp();
            }
            // else if ...
        }
        else if (entity.getSettings().isUsingController()) {
            if (entity.getControllerInput().getMovementJoystick().isHeldDown()) {
                moveDown();
            }
            // ...
        }
    }
}

I don't want to have to care about all of those things. If they're using Dvorak or some other keyboard mapping, suddenly it doesn't work.

It only gets marginally better if I swap it from isWKeyPressed to isUpKeyPressed, but I still don't want to have to manage all of those things, figuring out which input method to use.

I'd like to abstract all that business away, I (being the game engine) want to be as dumb and as lazy as possible. So, I thought, perhaps, I could create something like a MovementInputComponent as a base type for a KeyboardMovementInputComponent or ControllerMovementInputComponent.

public interface MovementInputComponent {

    boolean isMovingUp();

    boolean isMovingDown();

    boolean isJumping();
}

Which makes it, again, a little bit better, changed to

void update(GameEntity entity) {
    if (entity.getInputComponent().isMovingUp()) {
        moveUp();
    }
    else if (entity.getInputComponent().isJumping()) {
        jump();
    }
    // ...
}

We don't need to check which type of input the player is using (keyboard/controller), which also means we don't need to muddy up the system code. We're doing better, but still not great, because...

I want to be able to host this in a client-server environment; or maybe just bundled all together on the users PC; or maybe have a thin-client on their mobile device. But the game engine stays the same. Nothing must change within the game engine, just because the user is using their phone, because it's still the same game. Same game, same rules.

Keeping in mind the possibility that this could be in a client-server architecture, I would need some way of being able to tell what the client is doing, potentially across the world. Okay, we'll make a RemoteKeyboardMovementInputComponent that looks something like

class RemoteKeyboardMovementInputComponent implements MovementInputComponent {

    private final Socket clientSocket;

    // ...

    @Override
    public void isMovingUp() {
        clientSocket.sendCheckForUpKeyPressedPacket();
        return waitForResponse(clientSocket);
    }
}

The system still has no idea on the mechanics of how this is being generated, but now we've added real, hard latency into the mix. If we have to send packets of data to request what keys the client has pressed, we're in for a 2FPS game, which just won't do. This leads me to...

Why am I still in the business of the mechanics of actions?

I thought I was getting away from having to worry about how a key was being pressed, but I just moved the problem further downstream. I don't want to have to worry about any of it. I don't want to see the word Keyboard or Controller anywhere in the game engine, because it shouldn't care. It should only care about the game world; nothing more, nothing less.

Fine, we know we haven't solved the root cause. I (the game engine) know that somehow, somewhere, I'm getting inputs from something, but I don't care about where. I just care about the intent; as well, I don't want to potentially poll some external resource for the current state, I want it to be readily available. This led me to the idea of having some sort of command queue that I could poll.

However, I still want to separate the concerns of the game engine from the game server; where the engine cares only about the things that happen within the world, and the server cares only about the packets that are being sent and received.

class GameServer implements Runnable {

    final PacketHandlerRepository packetHandlers;

    @Override
    public void run() {
        while (true) {
            for (Socket client : connectedClients) {
                if (client.hasPendingPacket()) {
                    Packet packet = client.getNextPacket();
                    PacketHandler handler = packetHandlers.getPacketHandler(packet);

                    handler.handle(packet);
                }
            }
        }
    }
}

class MoveUpPacketHandler implements PacketHandler {

    final Queue<GameAction> actionQueue;

    void handle(MoveUpPacket packet) {
        actionQueue.add(new MoveUpAction());
    }
}

So now we can share the action queue between the server and the engine.

class GameEngine implements Runnable {

    final Queue<GameAction> actionQueue;

    GameEngine(Queue<GameAction> actionQueue) {
        this.actionQueue = actionQueue;
    }

    @Override
    public void run() {
        // ...
    }
}

And now we get to the heart of the question (and perhaps, the reason for my initial title of Multiple inputs per frame...); I have this action queue, filled with the intentions of the players.

How should we update the state of the components?

Should we pop from the queue the next action, handle the action, and then continue processing through the systems like normal?

class MoveUpActionHandler implements ActionHandler<MoveUpAction> {

    final ComponentFinder componentFinder;

    void handle(MoveUpAction action) {
        Position position = componentFinder.findComponent(action.getEntityId(), Position.class);

        position.moveUp();
    }
}

// in GameEngine
while (true) {
    GameAction action = actionQueue.pop();
    ActionHandler handler = actionHandlers.getHandler(action);

    handler.handle(action);

    for (GameSystem system : systems) {
        for (GameEntity entity : entities) {
            system.update(entity);
        }
    }
}

Looking at this code, it seems vaguely reminiscent of a system, but instead of it being driven by components, its driven by actions; which means it's not really a system, it just kind of behaves like one (like how a tractor kind of behaves like a Ferrari).

Do we take multiple actions per frame? How would we decide what the right number is? Do we do something different all together that I haven't thought of?

\$\endgroup\$
1
  • \$\begingroup\$ Your title seems to have nothing to do with your question. If it does, could you further explain what you mean? \$\endgroup\$
    – PSquall
    Commented Sep 20, 2017 at 7:55

2 Answers 2

2
\$\begingroup\$

There may be a bit of a misunderstanding of how an ECS is intended to work here.

You shouldn't ever need to "poll" for state updates on components. In the ECS model, the components are the state. The systems should mutate components in-place or otherwise synchronize their changes to the components during their updates.

e.g., the Position component in your example should always contain the up-to-date position of its entity. Systems like physics should update the Position components of anything the system wants to move; any system updating later in sequence will thus automatically see the up-to-date position inside the Position component.

On a vaguely-related note, your main loop is a bit scary. You shouldn't loop over every entity for every system (an N*M operation!) but instead have each system keep its own internal list of relevant entities. e.g., the physics system probably only cares about entities that have both Position and Shape. When entities (or components) are created/destroyed, notify systems and let them register/deregister the entity if appropriate. The system can then keep tuples of component references (e.g., that physics system might have an array of struct{position*, shape*} objects so it can directly read and mutate the components/state it cares about; it might even make copies of immutable state into its own data structure for each more convenient or efficient access).

\$\endgroup\$
1
  • \$\begingroup\$ I've updated the question with some additional details (sorry about the length). \$\endgroup\$
    – Zymus
    Commented Sep 21, 2017 at 4:54
1
\$\begingroup\$

Input can be handled several ways at the lowest level and it really depends on the engine's design. Some engines provide a way to inject input, placing the burden of developing that code as a part of each individual application while other engines may expose a cross-platform module that allows the engine to poll/capture input from various devices as a part of its own main loop.

That aside, you're definitely on the right track by adding a translation step to your input processing if you wish to support any type of configurable key-binding solution. This intent system should be able to either poll for specific input state or react to a sequence of input state changes depending on the needs of the key-binding configuration.

The intent system effectively toggles various game specific state ON or OFF. This state could be something which is stored either on a component or a local system variable during the a system's update phase.

But whether your game runs in a client-server simulation or simply a local one, your local game simulation should always react to movement intent. In other words, when a user presses the W key, the user should move forward. This movement should be simulated locally to avoid any problems with the user's game play experience because of latency.

So then how do we make sure the user isn't cheating in a client-server simulation? This is precisely where periodic position validation needs to happen by the server.

Each frame, your code should package up all movement intent that was requested and send that along with the player's current position to the server. The server simulates the movement just like the client and compares its results to that of the client's position. If the server deems the client's position to be within a reasonable margin of error to the server's simulation, the server accepts the client's position and responds with nothing. Should the client's position be incorrect, the server will request the client to reset the player's position to the server's last good position (aka rubber bands the user back to a prior point).

So in a client-server simulation, the only difference here is the fact the server has authority to force the client to move the player to a specific position; otherwise, it simply performs the same simulation as the client and validates that the client isn't where it shouldn't be.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .