3
\$\begingroup\$

I've been implementing an Entity Component System into my custom toy engine so I can learn more about it. Currently I have the following problem:

I can't think of a way of implementing input as a system. Currently I just check for key presses in the main loop, like this:

if (glfwGetKey(Window, GLFW_KEY_A) == GLFW_PRESS)
    Camera.Translate(glm::vec2(-5.0f, 0.0f) * static_cast<float>(DeltaTime));

Here I check if key A is pressed, and if it is I move the camera, which isn't a component associated with an entity.

So what I thought is creating an Entity with a Input component associated (this component would have mouse coordinates, keys pressed, etc):

ECS::Entity * InputManager = EntityWorld->create();
InputManager->assign<Input>();

Then, each frame, after polling events, I would update the Entity directly like this:

InputManager->get<Input>()->MousePosition = MousePosition;
InputManager->get<Input>()->MouseWheelDelta = MouseWheelDelta;
....

Then, I'd have an InputSystem, where I can check mouse position, if some key is pressed, etc. But what I don't understand is, if the camera is not a component, how could I make it move now that I handle input through the InputSystem? If I make the camera a component, how would I implement all the functionality to move it around, and zoom in and out?

Let me know if something it's not clear.

\$\endgroup\$
  • \$\begingroup\$ Is there an issue with not handling the camera with your ECS? \$\endgroup\$ – Vaillancourt May 22 at 19:56
  • \$\begingroup\$ Well, I've been thinking about it, but I'm not completely sure how to do it. Should I create a CameraSystem, which has functions to move around, get the view matrix, zoom in, etc, and then each tick check for Input retrieving the entity that has that component? I don't know if that would be a good solution. \$\endgroup\$ – Sergio Dorado May 22 at 20:54
4
\$\begingroup\$

I think you're pretty close.

Here is one option, which I'd try to implement first. You need 2 components

  • Input
  • Camera

I assume you'd create an entity and add them like this:

ECS::Entity * UserCameraManager = EntityWorld->create();
UserCameraManager->assign<Input>();
UserCameraManager->assign<Camera>(getMainUserCamera());

Each frame, your InputSystem fetches all relevant information regarding the input (e.g. mouse, keyboard, joystick), and stores it in the Input component.

Then, the system that takes care of the camera (e.g. CameraSystem), updates all the entities that have both of these components, Input and Camera. (Yes, that's going to be only one pair, but that's ok.)

The update(Input, Camera) function will take the 'raw' input from the Input component, and 'translate' it to commands to move the Camera

CameraSystem::update(Input input, Camera camera)
  if ( input.isKeyPress(Input::A) || input.getMouseXScreenRatio() < 0.01 )
      camera.getCamera()->Translate(glm::vec2(-5.0f, 0.0f) * static_cast<float>(DeltaTime));

Now, it's a bit silly because all the instances of Input would be the same, so you might want to bend your ECS rules a bit to have an 'empty' Input component and use it only as something like a tag, letting those system that needs it poll an actual, more global InputManager instead of the component. (Or use it to store a reference to the more global input manager.)

So instead, each frame, the InputSystem fetches all relevant information regarding the input (e.g. mouse, keyboard, joystick), and stores it within itself, ready for polling.

ECS::Entity * UserCameraManager = EntityWorld->create();
UserCameraManager->assign<Input>(getGlobalInputManager());
UserCameraManager->assign<Camera>(getMainUserCamera());

CameraSystem::update(Input input, Camera camera)
  if ( input.getInputManager()->isKeyPress(Input::A) || input.getInputManager()->getMouseXScreenRatio() < 0.01 )
      camera.getCamera()->Translate(glm::vec2(-5.0f, 0.0f) * static_cast<float>(DeltaTime));

We programmers love layers. So let's add another layer onto it.

You need 3 components

  • Input
  • Camera
  • CameraController

Let's re-use the InputSystem from the previous option, which updates the Input component (or a more global alternative).

Then let's use a System that will translate the 'input' into 'actions':

SystemCameraControl::update(Input input, CameraController cameraController)
  cameraController.resetValues()
  if ( input.getInputManager()->isKeyPress(Input::A) || input.getInputManager()->getMouseXScreenRatio() < 0.01 
    cameraController.moveLeft(5)

Then the other system takes care of converting the 'actions' into 'implementation':

CameraSystem::update(CameraController cameraController, Camera camera)
  if ( cameraController.shouldMove() )
    camera.getCamera()->Translate(cameraController.getDisplacementPerSec() * static_cast<float>(DeltaTime));

This option is really there to let you add some other components that will control the camera in a different way: when you'll want to have your camera automatically follow a spline, for instance, it might be a bit easier to think about it like this.

And this is to hint you could eventually have other entities be controlled: the main character will be moved like this, but you'll also have to move AI characters, for instance.

Of course, you don't need that extra component, at least now, but that's an option.


To address your questions from the comments:

When adding the Camera component to the entity in your example, the getMainUserCamera() function would return a Camera object? I thought that components are structs with data that we are not supposed to have an instance of, and should be managed with the systems.

There are no rules that say that you have to put everything in your ECS: you can limit the ECS to what makes sense. IMHO, it makes perfect sense to have your rendering engine or your physics engine not an integral part of your ECS. You can use your ECS components to refer to objects in your non-ECS software parts.

This is the case in this example, getMainUserCamera() would return a reference to the Camera that is part of this non-ECS software part. Your Camera component is used to link the two software parts together. It's still 'data'. The data is a pointer to something else.

If it makes more sense to you, you can have the CameraSystem create the Camera component, since they'll most likely have to interface with the Rendering part of your software.

How would the InputSystem fetch the raw input? The only thing I can think of is accessing the entity that has the input component directly before ticking all systems in the main loop, not in a system.

Assuming that you don't want to repeat Input data in eacn Input component, you could have something like this (pseudo-c++):

class InputState
{
  public:

    enum Key
    {
      A = GLFW_KEY_A,
      B, C //...
      Z, 
      NUM_KEYS = Z - A + 1
    };

    enum Mouse
    {
      ButtonLeft,
      ButtonMiddle,
      ButtonRight,
      NNUM_MOUSE_BUTTONS
    };

    enum ButtonState
    {
      DOWN,
      JUST_DOWN,
      RELEASED,
      JUST_RELEASED
    };

    bool isKeyDown(Key aKey) { return mKeyState[aKey - 'A'] == DOWN || mKeyState[aKey - 'A'] == JUST_DOWN; }
    bool isKeyJustDown(Key aKey) { return mKeyState[aKey - 'A'] == JUST_DOWN; }
    bool isMouseDown(Mouse aMouseButton) // ...

    // This has to be called before anything related to input is updated
    void perFrameUpdate()
    {
      // Update the keys
      for ( char i = Key::A; i <= Key::Z; ++i )
      {
        int vectorIndex = i - Key::A;
        bool isKeyDown = glfwGetKey(Window, i) == GLFW_PRESS;

        ButtonState lastKeyState = mKeyState[vectorIndex];

        if ( isKeyDown && lastKeyState == ButtonState::JUST_DOWN )
          mKeyState[vectorIndex] = ButtonState::DOWN;
        else if ( isKeyDown && ( lastKeyState == ButtonState::RELEASED || lastKeyState == ButtonState::JUST_RELEASED )
          mKeyState[vectorIndex] = ButtonState::JUST_DOWN;
        else if // ...
          // ...

      }

      // Do the same for mouse
    }

  private:

    std::vector<ButtonState> mKeyState;
    std::vector<ButtonState> mMouseState;

}


// This is your `Input` component
struct Input 
{
  explicit Input( InputState& aGlobalState ) : mInputState( aGlobalState ) {}

  InputState& mInputState;
}

// This is your `Camera` component
struct Camera
{
  explicit Camera( Render::Camera* aCamera ) : mCamera( aCamera ) {}

  Render::Camera* mCamera;
}    


class SystemCamera
{
  public:
    static void update_CameraInput( std::vector<ECS::Entity*>& aEntities, double DeltaTime )
    {
      Camera& camera = aEntities->get<Camera>();
      Input& input = aEntities->get<Input>();

      if ( input.mInputState.isKeyDown(InputState::A) || input.mInputState.getMouseXScreenRatio() < 0.01 )
          camera.mCamera->Translate(glm::vec2(-5.0f, 0.0f) * static_cast<float>(DeltaTime));
    }
}

// Main loop
class Game
{
  public:

    void init()
    {
      mMainCamera = // RenderManager::GetOrCreateMainCamera();

      ECS::Entity * UserCameraManager = EntityWorld->create();
      UserCameraManager->assign<Input>(mInputState);
      UserCameraManager->assign<Camera>(mMainCamera);
      mEntities.push_back( UserCameraManager );
    }

    void run()
    {
      while(true)
      {
        mInputState.perFrameUpdate();

        // No need for an additional system to update the `Input` component, as they all refer to the same up-to-date mInputState.
        std::vector<ECS::Entity*> entitiesThatHaveInputAndCamera = getRelevantEntities<Camera, Input>(mEntities);

        SystemCamera::update_CameraInput(entitiesThatHaveInputAndCamera, DeltaTime);
      }
    }

  private:

    InputState mInputState;
    std::vector<ECS::Entity*> mEntities;

    Render::Camera* mMainCamera;
}
\$\endgroup\$
  • \$\begingroup\$ I think I get it now, but I have two questions. First, when adding the Camera component to the entity in your example, the getMainUserCamera() function would return a Camera object? I thought that components are structs with data that we are not supposed to have an instance of, and should be managed with the systems. Second, how would the InputSystem fetch the raw input? The only thing I can think of is accessing the entity that has the input component directly before ticking all systems in the main loop, not in a system. Thanks a lot for your time. \$\endgroup\$ – Sergio Dorado May 23 at 7:35
  • \$\begingroup\$ @SergioDorado I have updated my answer. I admit that there are some incoherences in the first part of the answer regarding input*: InputManager, InputSystem; I'll have to come back to the answer to fix it. My 'last' example should be consistent. There is only the InputState which helps understand that it's not a System. It's not a singleton, as you could have one per window. The value added over taking the input directly from GLFW is that you have a state management from frame to frame. If there is still anything unclear, please ask :) \$\endgroup\$ – Vaillancourt May 23 at 16:51
  • \$\begingroup\$ Everything is clear now, I'll start to implement it and see how it goes. Thanks a lot again for your time and for your great answer :D \$\endgroup\$ – Sergio Dorado May 24 at 7:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.