Player character re-implemented
So the player controller code is effectively rewritten back to the level it was at before. The player can stand still, turn on the spot, run in any direction, walk in any direction (stopping at edges), jump and fall with air controls.
It also causes the player to stop walking or running when it hits an obstacle square on, and prevents the player from starting to walk or run if there is an obstacle already in the way.
The rotation is all based on the previous and current positions, rather than the direction that the player is pressing keys for, which leads to nicer animation and visuals as the player slides around corners and so on.
This time, the entire state machine in handled in a single file. This file is going to grow rather large as we add new states and abilities, but it does mean I have a far better overview of what is going on now and much more opportunity to share code between states. Here it is in its current majesty:
#include "PcStateMachine.h"#include "PcData.h"#include "application/GameState.h"#include "maths/Camera.h"PcStateMachine::PcStateMachine(PcData &data) : data(data), curr(PcState::Null), next(PcState::Null), hv(0, 0, 0), vv(0), ts(0){ delegate.connect(this, &PcStateMachine::animationEvent, data.animation.controller.event); next = PcState::Idle;}void PcStateMachine::update(GameState &state, float delta){ if(next != PcState::Null && curr != next) { if(canEnterState(state, next, delta)) { enterState(state, next, delta); } else { enterState(state, PcState::Idle, delta); } next = PcState::Null; ts = 0; pv = vectorLength(hv); if(pv < epsilon()) pv = data.settings.walkSpeed * delta; } ts += delta; data.rot.store(); data.rot.update(delta, callback(this, &PcStateMachine::rotationEvent)); data.animation.controller.store(); data.animation.controller.update(delta, data.animation.map); switch(curr) { case PcState::Idle: updateIdle(state, delta); break; case PcState::Falling: updateFalling(state, delta); break; case PcState::Running: updateRunning(state, delta); break; case PcState::Walking: updateWalking(state, delta); break; case PcState::Jumping: updateJumping(state, delta); break; default: data.kcc.move(state.physics, Kcc::NoFlags, Vec3(0, 0, 0)); break; } if(curr != PcState::Falling && curr != PcState::Jumping && !data.kcc.grounded()) { next = PcState::Falling; }}bool PcStateMachine::canEnterState(GameState &state, PcState attempt, float delta) const{ if(attempt == PcState::Running) { Vec3 v = data.inputVector(state.camera.ang.value().x) * (data.settings.runSpeed * delta); return data.kcc.canMove(state.physics, Kcc::NoFlags, v, tolerant_epsilon()); } if(attempt == PcState::Walking) { Vec3 v = data.inputVector(state.camera.ang.value().x) * (data.settings.walkSpeed * delta); return data.kcc.canMove(state.physics, Kcc::StayOnEdges, v, tolerant_epsilon()); } return true;}void PcStateMachine::enterState(GameState &state, PcState attempt, float delta){ PcState prev = curr; curr = attempt; AnimationId id = mapPcStateToAnimationId(curr, data.animation.ids); if(id != InvalidResourceId) { data.animation.controller.transitionTo(id, matchPcAnimationTransform(prev, curr)); }}void PcStateMachine::updateIdle(GameState &state, float delta){ data.kcc.move(state.physics, Kcc::NoFlags, Vec3(0, 0, 0)); Vec3 v = data.inputVector(state.camera.ang.value().x); if(vectorLength(v) > epsilon()) { if(data.turnAngle(v) >= 1.0f) { data.setRotation(v, 0.2f); next = PcState::Turning; } else { next = data.moveState(); } } if(data.controls.down(PcControls::Jump)) { next = PcState::BeginJump; }}void PcStateMachine::updateFalling(GameState &state, float delta){ if(data.controls.anyMovementDown()) { hv = data.inputVector(state.camera.ang.value().x) * pv; } else { hv = reduce(hv, pv); } vv -= data.settings.fallRate * delta; vv = limit(vv, -data.settings.fallSpeed * delta, data.settings.fallSpeed * delta); data.kcc.move(state.physics, Kcc::NoFlags, Vec3(hv.x, vv, hv.z)); data.updateKccRotation(); if(data.kcc.grounded()) { next = PcState::Landing; }}void PcStateMachine::updateRunning(GameState &state, float delta){ if(data.controls.anyMovementDown()) { hv = data.inputVector(state.camera.ang.value().x) * (data.settings.runSpeed * delta); } data.kcc.move(state.physics, Kcc::NoFlags, hv); data.updateKccRotation(); if(!data.kccMoved(tolerant_epsilon())) { next = PcState::Idle; } if(data.controls.down(PcControls::Walk)) { next = PcState::Walking; } if(data.controls.down(PcControls::Jump)) { next = PcState::Jumping; }}void PcStateMachine::updateWalking(GameState &state, float delta){ if(data.controls.anyMovementDown()) { hv = data.inputVector(state.camera.ang.value().x) * (data.settings.walkSpeed * delta); } data.kcc.move(state.physics, Kcc::StayOnEdges, hv); data.updateKccRotation(); if(!data.kccMoved(tolerant_epsilon())) { next = PcState::Idle; } if(!data.controls.down(PcControls::Walk)) { next = PcState::Running; } if(data.controls.down(PcControls::Jump)) { next = PcState::BeginJump; }}void PcStateMachine::updateJumping(GameState &state, float delta){ if(data.controls.anyMovementDown()) { hv = data.inputVector(state.camera.ang.value().x) * pv; } else { hv = reduce(hv, pv); } float inc = (1 - ts) * 3; vv = (inc * data.settings.jumpRate) * delta; vv = limit(vv, 0, data.settings.jumpSpeed * delta); data.kcc.move(state.physics, Kcc::NoFlags, Vec3(hv.x, vv, hv.z)); data.updateKccRotation(); if(ts >= 0.33f) { next = PcState::Falling; }}void PcStateMachine::rotationEvent(){ if(curr == PcState::Turning) { next = PcState::Idle; }}void PcStateMachine::animationEvent(const AnimationEventData &eventData){ switch(curr) { case PcState::Landing: next = PcState::Idle; break; case PcState::Running: case PcState::Walking: if(!data.controls.anyMovementDown()) next = PcState::Idle; break; case PcState::BeginJump: next = PcState::Jumping; break; default: break; }}The most observant of you will notice I've split out the horizontal velocity and the vertical velocity. Horizontal velocity is a Vec3 whose y component is always zero and vertical velocity is just a float. We compose these into a vector using (hv.x, vv, hv.z) at the last possible point. This allows us to treat the two as separate in terms of capping, increasing and decreasing everywhere else, which is what we want in this particular context.
You'll see most of the hard physics work is still handed off to the Kcc class (Kinematic Character Controller) which I'll have to try to make a longer post about sometime in the near future. It really is the powerhouse behind the player controller but is suitable for use in non-player characters as well and forms a fairly tight bridge between higher level concepts like running, walking etc and lower level phyiscs interactions between the player shape and the physics bodies that make up the level.
There are a couple of ways that changing state is delayed - animation events and rotation events. When the player enters the PcState::Turning state, the FadeValue rot value emits a callback when the rotation reaches its target and we use this to jump back to idle.
Similarly, animation events are defined in Charm, the model and animation editor and are used in various creative ways. For example, walking and running emit animation events on frames where the feet are close together and we only exit walk and run states during the firing of these events, so the player will always complete at least one step before stopping, even if the player only taps the key, avoiding some nasty flickering animations.
Or, more simply, BeginJump animation just emits an animation event at the end when the knees are fully bent, so we use this trigger to go from BeginJump to Jump, meaning the start of the actual Jump is triggered by the BeginJump animation completing.
You can just define these events in Charm in the animation workflow. It is possible to attach string data to them as well, although so far the context in which they are emitted has been sufficient here.
Using a PcData class to hold the state of the player allows me to control the player across a few places. There is a Pc class which actually inherits from Entity so allows the player to live in the list of generic Entities that the level maintains. PcStateMachine also access the PcData and is a member of the Pc class.
PcData is also a nice place to add a bunch of support functions that can be used as shortcuts to manipulating the player.
Edge grabbing and shimmying probably up next. I have implemented this before. I already have the edge data I need (see previous post). The big challenge and failure here before has been making animations that look good enough for this. We'll have to see how it goes this time. It might be a good time to start thinking about a whole new player model before I go any further. Lot of work involved in that though as I'm not an expert at this stuff and my models and animations are never quite as good as I'd like.
Thanks for stopping by.
Discussion