Move your Player with a Data-Driven Movement System in Babylon.js
In our previous adventures, we dynamically loaded game assets using components and systems. Now, it's time to inject some action into our game world by building a robust movement system. Get ready to watch your player characters gracefully glide, dash, and leap across the screen!
The MovementComponent
: Defining Motion
First, we need a way to store movement-related properties for our entities. Enter the MovementComponent
:
import { Component } from "../lib";
import { Vector3 } from "@babylonjs/core";
export interface MovementInput {
velocity: number[];
position: number[];
walkingSpeed: number;
rotationSpeed: number;
jumpingSpeed: number;
}
// MovementComponent.ts
export class MovementComponent extends Component {
velocity: Vector3;
position: Vector3;
acceleration: Vector3;
walkingSpeed: number;
rotationSpeed: number;
jumpingSpeed: number;
movementOverride: Vector3;
overrideTimer = 0;
constructor(data: MovementInput) {
const { position, walkingSpeed, rotationSpeed, jumpingSpeed } = data;
super(data);
this.velocity = Vector3.Zero();
this.position = Vector3.FromArray(position);
this.acceleration = Vector3.Zero();
this.walkingSpeed = walkingSpeed;
this.rotationSpeed = rotationSpeed;
this.jumpingSpeed = jumpingSpeed;
}
// Optional: Methods to manipulate position and velocity directly
setPosition(x: number, y: number, z: number) {
this.position = new Vector3(x, y, z);
}
setVelocity(vx: number, vy: number, vz: number) {
this.velocity = new Vector3(vx, vy, vz);
}
// Optional: Method to apply a force to the acceleration
applyForce(force: Vector3) {
this.velocity.addInPlace(force);
}
}
Key Properties:
velocity
: The entity's current velocity (speed and direction).position
: The entity's current position in 3D space.acceleration
: The rate of change of velocity.walkingSpeed
,rotationSpeed
,jumpingSpeed
: Parameters controlling how fast the entity moves.
The MovementSystem
: Bringing Motion to Life
Now, let's create the MovementSystem
. This system will process only entities that have all three, AssetComponent
, MovementComponent
and PlayerInputComponent
. These can be thought of as the system's data dependencies. It never sees entities that don't have all three components.
import { Entity, EntityManager, System } from "../lib";
import { PlayerInputComponent } from "../components/PlayerInputComponent";
import { MovementComponent } from "../components/MovementComponent";
import { AssetComponent } from "../components/AssetComponent";
export class MovementSystem extends System {
constructor(
entityManager: EntityManager,
componentClasses = [
PlayerInputComponent,
MovementComponent,
AssetComponent,
],
) {
super(entityManager, componentClasses);
this.componentClasses = componentClasses;
console.log("MovementSystem initialized");
}
updateMovement(
assetComponent,
movementComponent: MovementComponent,
deltaTime: number,
) {
// 1. Calculate the desired velocity based on input and walkingSpeed
const { walkingSpeed, velocity, position } = movementComponent;
const desiredVelocity = velocity.scale(walkingSpeed);
if (
assetComponent.mainMesh &&
movementComponent.position != assetComponent.mainMesh.position &&
velocity.length() == 0
)
assetComponent.mainMesh.position = movementComponent.position;
// 2. Calculate the acceleration needed to reach desired velocity
const acceleration = desiredVelocity
.subtract(velocity)
.scale(1 / deltaTime);
// 3. Limit acceleration to a maximum value if needed
const maxAcceleration = walkingSpeed; // Define your maximum acceleration
if (acceleration.length() > maxAcceleration) {
acceleration.normalize().scaleInPlace(maxAcceleration);
}
// 4. Update velocity with acceleration and deltaTime
movementComponent.velocity = velocity.addInPlace(
acceleration.scale(deltaTime),
);
if (isNaN(movementComponent.velocity.x)) {
// debugger;
}
// 5. Update position based on velocity and deltaTime
// movementComponent.position = position.addInPlace(
// movementComponent.velocity.scale(deltaTime),
// );
// console.log(movementComponent.velocity);
const a = assetComponent.mainMesh;
a.position.y = 0;
a.moveWithCollisions(movementComponent.velocity);
movementComponent.position = a.position;
}
protected processEntity(entity: Entity, deltaTime: number): void {
const movementComponent = entity.getComponent(MovementComponent);
const inputComponent = entity.getComponent(PlayerInputComponent);
const assetComponent = entity.getComponent(AssetComponent);
if (assetComponent && assetComponent.mainMesh) {
movementComponent.velocity = inputComponent.movementVector;
this.updateMovement(assetComponent, movementComponent, deltaTime);
}
}
}
Explanation:
processEntity(entity, deltaTime)
: This method, called by theEntityManager
, processes each entity that has both aMovementComponent
and aPlayerInputComponent
.updateMovement(...)
: This function encapsulates the movement logic:- Calculates the desired velocity based on player input and the entity's speed.
- Calculates the acceleration needed to reach that velocity.
- Limits acceleration if necessary to prevent unrealistic movement.
- Updates the entity's velocity and position based on acceleration and time elapsed (
deltaTime
).
Defining Movement in Our scene.json
Let's assume our scene.json
now includes movement data for the player:
{
"entities": {
// ... (World entity)
"Player": {
"components": {
// ... (Asset component)
"Movement": {
"position": [0, 0, 0],
"walkingSpeed": 1,
"runningSpeed": 800,
"jumpingSpeed": 5,
"rotationSpeed": 50
},
// ... (InputKeyboard component)
}
}
}
}
Bringing It All Together: Movement in Harmony
When our game runs:
- The
SceneManager
loads thescene.json
and instantiates theMovementSystem
. - The
EntityManager
calls theMovementSystem
'sprocessEntity
method for each entity with the required components. - The
MovementSystem
reads input data, calculates movement, and updates the positions of our entities, bringing them to life with smooth, data-driven motion.
In Sum
We've successfully built a flexible movement system driven by data! By defining movement properties in our scene.json
and using components and systems, we've decoupled our game logic, making it easier to modify and extend. Experiment, iterate, and watch your game world come alive with dynamic, data-driven motion!