• entries
    743
  • comments
    1924
  • views
    580349

Character Controller - shadows, animated meshes and state machines

Sign in to follow this  

1119 views

shade.jpg

I've integrated an animated model as the main character since last time, created a nice, tidy state machine to handle the character states and added back in shadow mapping. Obligatory video showing all this here:

[media]
[/media]

Finite state machines are an excellent way to look after a player character as they allow you to tightly control the flow from one state to another, synchronise animations with states and keep each state as independent of the others as possible, easing expansion into more complex character behaviours.

My base PcState has a fairly simple interface:

class PcState{public: PcState(PcData &data, PcStateMachine &machine); virtual ~PcState(); enum Type { Idle, Fall, Run, Turn, Land, Null }; virtual void enter(); virtual void leave(); virtual void update(FrameParams ¶ms, Frame &frame, float delta) = 0; virtual void animationEvent(const AnimationEventData &event); virtual void rotationFinishedEvent();protected: Vec3 inputVector(const FrameParams ¶ms) const; float turnAngle(const Vec3 &v) const; void setRotation(const Vec3 &v, float speed); void checkFall(); PcData &data; PcStateMachine &machine;};PcData is a public structure containing all of the actual state of the player, a pattern I like because I can pass this easily around different classes and functions making it easier to split the functionality across different units.

Most of the virtual methods are just stubs that specific states can optionally override if they need to. Synchronising via the animation system is one way to synchronise state changes. A simple example is the LandState - this state is entered at the end of FallState, assuming FallState has been running for more than one tenth of a second:

void FallState::update(FrameParams ¶ms, Frame &frame, float delta){ Vec3 v = inputVector(params); Vec3 d = v * data.settings.runSpeed * delta; if(d.y <= 0) { d.y = -10 * delta; } data.kcc.move(frame.physics, d); setRotation(v, 0.1f); t += delta; if(data.kcc.grounded()) { machine.setNextState(t > 0.1f ? PcState::Land : PcState::Idle); }}LandState starts up the Land animation in its enter() method. The Land animation is set up to trigger an AnimationEvent at the end of its animation, which is then listened out for by the LandState to use as a trigger to return back to IdleState:

void LandState::enter(){ data.animation.controller.transitionTo(data.animation.map.id("Land"), false);}void LandState::update(FrameParams ¶ms, Frame &frame, float delta){ data.kcc.move(frame.physics, Vec3(0, 0, 0)); checkFall();}void LandState::animationEvent(const AnimationEventData &event){ machine.setNextState(PcState::Idle);}A more complicated example is RunState. The Run animation fires animation events off at each point in the cycle that the two feet are together in the middle position. We only want to stop running when we are at one of these points, so that we take a complete step even if we just tap the key, so RunState only allows us to return to IdleState during processing of an animation event:

void RunState::update(FrameParams ¶ms, Frame &frame, float delta){ if(data.controls.anyMovementDown()) { v = inputVector(params); } data.kcc.move(frame.physics, v * data.settings.runSpeed * delta); setRotation(v, 0.1f); checkFall();}void RunState::animationEvent(const AnimationEventData &event){ if(!data.controls.anyMovementDown()) { machine.setNextState(PcState::Idle); }}The actual AnimationEventData is irrelevant in these examples, since context provides all the information we need, but my model editor is set up to allow you to set string data on any animation event, which is provided via the AnimationEventData parameter, which may be useful for more complex things in the future.

Similarly, PcData contains a BlendValue which looks after transforming a quaternion from one value to another smoothly over time, and has the facility to trigger a callback when the transformation is completed. Some pipework elsewhere causes this to tell the PcStateMachine to call the rotationFinishedEvent method on the current PcState, which can be used by TurnState to signal that it should return to IdleState:

void TurnState::update(FrameParams ¶ms, Frame &frame, float delta){ data.kcc.move(frame.physics, Vec3(0, 0, 0)); checkFall();}void TurnState::rotationFinishedEvent(){ machine.setNextState(data.controls.anyMovementDown() ? PcState::Run : PcState::Idle);}TurnState is entered from IdleState if the requested input vector direction is beyond a certain angular difference from the direction the player is facing at the time, so you can spin on the spot nicely.

void IdleState::update(FrameParams ¶ms, Frame &frame, float delta){ Vec3 v = inputVector(params); data.kcc.move(frame.physics, Vec3(0, 0, 0)); if(vectorLength(v) > epsilon()) { if(turnAngle(v) >= 1.0f) { setRotation(v, 0.2f); machine.setNextState(PcState::Turn); } else { machine.setNextState(PcState::Run); } } checkFall();}Just a couple of examples of using external inputs to synchronise state changes there. It all ends up feeling very unentangled and easy to keep tight control of as we add new behaviours to the player.

The shadows are straightforward simple shadow mappping. You can see the shadow map briefly in the video when I turn the debugging on, although its hard to interpret since I'm storing the depth value in all four components of the colour using a packing and unpacking algorithm to add precision without having to rely on R32F format textures being available.

I've decided to stick with top-down shadows only in this game. Firstly, the style is going to be very cartoony, secondly a top-down shadow gives you an excellent visual cue as to where the character is while in the air and lastly it simplifies a lot of issues with shadow mapping that are otherwise hard to solve without limiting the geometry that your levels can support.

The shadows aren't quite perfect - you can see a few artifacts around the edges of the curved shapes when close to the floor, but they are plenty good enough for me. I'm using a single 2048x2048 texture for the shadow map with a 3x3 PCF filter in the video, but results with 1024x1024 are also quite acceptable. I'm employing the normal offset trick which seems to work very well in this particular context.

Not sure if the current character model is going to be hanging around for long, so wanted to post a video while he was still alive and kicking since he is quite cute (if I do say so myself). I want to start work on inverse kinematics for foot placement and rag-doll generation soon, and will need a much simpler character model for these so I can see what is going on more clearly.

But all in all, feels very solid and robust so far and the codebase is nicely structured.

Thanks for stopping by.
Sign in to follow this  


0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now