I think you're pretty close.
Here is one option, which I'd try to implement first. You need 2 components
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;
}