Player Input and Movement in Babylon.js
In the previous article, we dynamically loaded assets and even got our entities moving. Now, let's dive into the crucial aspect of capturing player input and translating it into meaningful actions within our game world. We'll build a flexible, data-driven input system using our component-based architecture.
Capturing Keyboard Input: The InputKeyboardSystem
Now, let's build a system to handle keyboard input:
import { Entity, EntityManager } from "../lib";
import { InputKeyboardComponent } from "../components/InputKeyboardComponent";
import { PlayerInputComponent } from "../components/PlayerInputComponent";
import { Vector3 } from "@babylonjs/core";
export class InputKeyboardSystem {
inputComponent: InputKeyboardComponent | undefined;
playerInputComponent: PlayerInputComponent | undefined;
constructor(entityManager: EntityManager) {
const playerEntity = entityManager.playerEntity as Entity;
const playerInputComponent =
playerEntity.getComponent(PlayerInputComponent);
const keyboard = playerEntity.getComponent(InputKeyboardComponent);
this.inputComponent = keyboard;
this.playerInputComponent = playerInputComponent;
document.addEventListener("keydown", this.handleKeyDown.bind(this));
document.addEventListener("keyup", this.handleKeyUp.bind(this));
window.addEventListener("blur", () => {
for (const key in this.inputComponent.keys) {
this.inputComponent.keys[key] = false;
}
this.updateMovementVector(this.inputComponent);
});
console.log("InputKeyboardSystem initialized");
}
private handleKeyDown(event: KeyboardEvent) {
const inputComponent = this.inputComponent;
const { keys, keyMappings } = inputComponent;
const key = keyMappings[event.key.toLowerCase()];
if (!key) return;
inputComponent.keys[key] = true;
this.updateMovementVector(inputComponent);
}
private handleKeyUp(event: KeyboardEvent) {
const inputComponent = this.inputComponent;
const { keyMappings } = inputComponent;
const key = keyMappings[event.key.toLowerCase()];
if (!key) return;
inputComponent.keys[key] = false;
this.updateMovementVector(inputComponent);
}
private updateMovementVector(inputComponent: InputKeyboardComponent) {
this.playerInputComponent.movementVector = new Vector3(
(inputComponent.keys.right ? 1 : 0) - (inputComponent.keys.left ? 1 : 0),
0, // Assuming 2D movement for simplicity
(inputComponent.keys.forward ? 1 : 0) -
(inputComponent.keys.backward ? 1 : 0),
);
this.playerInputComponent.updateActions(inputComponent.keys);
}
}
Explanation:
- The
InputKeyboardSystem
listens forkeydown
andkeyup
events on the document. - It uses a
keyMappings
object (presumably defined in anInputKeyboardComponent
) to map key presses to meaningful actions like "forward," "backward," "left," and "right." - Based on the pressed keys, it updates a
movementVector
in aPlayerInputComponent
. This vector will be used by other systems, like our movement system, to move the player.
The PlayerInputComponent
: A Hub for Input Data
// PlayerInputComponent.ts
import { Component } from "../lib";
import { Vector3 } from "@babylonjs/core";
import { InputKeyboardComponent } from "./InputKeyboardComponent";
import { InputGamePadComponent } from "./InputGamePadComponent";
export class PlayerInputComponent extends Component {
public inputType = "keyboard";
public actionToKeyMap: any;
public currentActions: any;
public movementVector = new Vector3(0, 0, 0);
public inputSource: InputKeyboardComponent | InputGamePadComponent;
constructor(data) {
super(data);
this.currentActions = data.currentActions;
this.actionToKeyMap = data.actionToKeyMap;
}
updateActions(keys: any) {
for (const action in this.currentActions) {
const key = this.actionToKeyMap[action];
const pressed = keys[key];
this.currentActions[action] = pressed || false;
}
}
}
This component stores the player's current input state, including the movementVector
and any actions triggered by key presses.
The PlayerInputSystem
: Translating Input into Actions
import { Entity, EntityManager, System } from "../lib";
import { PlayerInputComponent } from "../components/PlayerInputComponent";
import { AnimationComponent } from "../components/AnimationComponent";
export class PlayerInputSystem extends System {
constructor(entityManager: EntityManager) {
super(entityManager, [PlayerInputComponent, AnimationComponent]);
console.log("PlayerInputSystem initialized");
}
processEntity(entity: Entity, deltaTime: number): void {
const playerInputComponent = entity.getComponent(PlayerInputComponent);
const animationComponent = entity.getComponent(AnimationComponent);
const movement = playerInputComponent.movementVector;
const still = !movement.length();
let t = "idle";
const { attack, shield, jump, Roundhouse_Low, roll } =
playerInputComponent.currentActions;
// console.log(playerInputComponent.currentActions.Roundhouse_Low);
if (still) {
if (attack) {
t == "attack3";
} else if (shield) {
t = "idle_shield";
} else if (jump) {
t = "jump";
} else if (Roundhouse_Low) {
t = "Roundhouse_Low";
} else {
t = "idle";
}
} else {
// moving
if (shield) {
t = "run_shield";
} else if (roll && movement.x && !movement.z) {
t = "straf";
} else if (roll) {
t = "roll";
} else {
t = "run";
}
}
if (animationComponent.targetAnimation != t)
animationComponent.targetAnimation = t;
}
}
This system processes entities with PlayerInputComponent
and AnimationComponent
components. It reads the player's input state and triggers corresponding animations.
Defining Input in our scene.json
Finally, we'd define our input mappings and actions in our scene.json
:
{
"entities": {
// ... other entities
"Player": {
"components": {
// ... other components
"PlayerInput": {
"type": "keyboard",
"currentActions": {
"attack": false,
"shield": false,
"jump": false,
"Roundhouse_Low": false,
"roll": false
},
"actionToKeyMap": {
"Roundhouse_Low": "control"
}
},
// ... other components
}
}
}
}
In Sum
We've built a responsive and data-driven player input system! By separating input handling, action mapping, and game logic into distinct components and systems, we've created a flexible and maintainable architecture. Now, our game can react to player input, triggering actions and animations that breathe life into our game world. Keep experimenting, keep iterating, and keep those creative juices flowing!