Godsend Programming #1: Character Controller

In this entry, I will be discussing the way I implemented the character controller and the dash mechanic.

Box2D and platformers

The first thing to consider when tasked with the character controller is the way your character should move and how accurate that is against a realistic physics simulation. This topic seems to have caused a discussion between developers and there are two split camps in terms of using Box2D for 2D platformer games. A good example to illustrate this is the article Why Using a Physics Engine for a 2D Platformer is a Terrible Idea (Steffen Itterheim, 2013). While the author argues that physics engines and platformers do not work well together, the comments section of the article argues the opposite point.

Starting off with the project, it already had a basic character controller implemented, which makes use of what Box2D has to offer. However, the controls present feel floaty and the character slides around the screen, as well as falls really slowly due to the damping applied.

At first, I tried to adjust the parameters in this implementation to make the character feel more responsive, but adjusting to the physics engine seemed to be tough work. I was further biased towards not using Box2D for the character controller by my previous experience on a platformer called Sailing in a Parking Lot (Mindaugas Kadzys, 2015) that I did a while back. It suffered from the same floaty controls as the starting controller that I received.

Doing some research into articles such as the one mentioned earlier suggested that I might not want to rely on physics for the character controller, as the character movement is simply not realistic in platformers, nor should it be. Instead of applying forces and impulses to the character, I implemented a controller by calculating and setting velocities directly, such as:

void CGCObjPlayer::DoHorizontalMovement( f32 fMovementDirectionX, b2Vec2& rv2NewVelocity, f32 fTimeStep )
{
	b2Vec2 v2PreviousVelocity = GetPhysicsBody()->GetLinearVelocity();
	//set new velocity based on previous as a start
	rv2NewVelocity.x = v2PreviousVelocity.x;
	rv2NewVelocity.y = v2PreviousVelocity.y;

	//if player wants to move horizontally
	if( 0.0f != fMovementDirectionX )
	{
		//check if we are moving in the same direction as before
		//this determines if we are speeding up or slowing down to counter-act the directional velocity
		if( v2PreviousVelocity.x * fMovementDirectionX >= 0 )
		{
			//continue speeding up or cap speed at max
			if( fabs( v2PreviousVelocity.x ) < m_fTopSpeedX )
			{
				rv2NewVelocity.x += m_fSpeedIncreaseX * fMovementDirectionX;

				if( fabs( rv2NewVelocity.x ) > m_fTopSpeedX )
				{
					rv2NewVelocity.x = m_fTopSpeedX * fMovementDirectionX;
				}
			}
			else
			{
				rv2NewVelocity.x = m_fTopSpeedX * fMovementDirectionX;
			}
		}
		//if we are switching directions, just calculate the speed equation because we can't reach top speed if we're slowing down
		else
		{
			rv2NewVelocity.x += ( m_fSpeedIncreaseX * fMovementDirectionX ) * 1.5f;
		}
	}
	//if no horizontal input, slow down the character
	else
	{
		//don't stop character mid-air
		if( true == IsGrounded() )
		{
			//stop the character once they reach a slowness threshold
			if( fabs( v2PreviousVelocity.x ) < m_fMinSpeedX )
			{
				rv2NewVelocity.x = 0.0f;
			}
			else
			{
				rv2NewVelocity.x = v2PreviousVelocity.x * m_fSpeedDecreaseX;
			}
		}
		//if player is on moving platform, keep that speed as a minimum
		if( true == IsOnMovingPlatform() )
		{
			//stop the character once they reach a slowness threshold
			if( fabs( v2PreviousVelocity.x ) < ( fabs( m_v2ExternalVelocity->x ) + m_fMinSpeedX ) )
			{
				rv2NewVelocity.x = m_v2ExternalVelocity->x;
			}
			else
			{
				rv2NewVelocity.x = v2PreviousVelocity.x * m_fSpeedDecreaseX;
			}
		}
	}
}

This resulted in a character controller that was appreciated by the rest of the team, and I thought I was good to go. However, having such a controller led to implications such as its compatibility with the rest of the physics world. The character would not move along moving platforms automatically, and a specific implementation to allow that was needed:

void CGCObjPlayer::ApplyExternalForceToPlayer(b2Vec2& v2ExternalForce)
{
	//check if first contact with moving platform
	if( 0 == m_uNoMovingCollisions )
	{
		//check if current force and new force are different to avoid multiple assignments
		if( nullptr == m_v2ExternalVelocity
			|| ( m_v2ExternalVelocity->x != v2ExternalForce.x || m_v2ExternalVelocity->y != v2ExternalForce.y ) )
		{
			m_v2ExternalVelocity	 = &v2ExternalForce;

			b2Vec2 v2CurrVelocity	 = GetPhysicsBody()->GetLinearVelocity();
			v2CurrVelocity.x		+= m_v2ExternalVelocity->x;
			v2CurrVelocity.y		+= m_v2ExternalVelocity->y;

			GetPhysicsBody()->SetLinearVelocity( v2CurrVelocity );
		}
	}

	m_uNoMovingCollisions++;
}

This meant that I needed to manually bind the moving platform to the player. Furthermore, it would mean that any other controller implemented in a similar fashion would need something similar in it. This goes for enemies, pushable blocks and anything else we might  want to add in the future.

Evaluating this approach, I feel like the character controls feel good and the game designer has fine control over how the player moves. That’s pretty much what’s good in the controller. What’s bad is this incompatibility with the rest of the world, and the need to write special code for interactions between different objects, actively bypassing the physics engine behaviour. There’s two issues with this. Firstly, it’s not how physics engines should be used, nor is it what they are made for to begin with. Secondly, the approach does not scale well, and might require more and more time to make everything compatible with each other in the future and it also increases the risk of unexpected bugs arising from this incompatibility.

I have further researched the topic and found out that the critically acclaimed game, Shovel Knight (2014), used Box2D as well. Regrettably, I have not played it myself and I am unsure as to how physics are used in that game. I plan to remedy this by trying out the game as soon as I have the chance.

However, this does raise a suspicion that a platformer controller can in fact be implemented well using physics. If so, I might have made a mistake of implementing it directly and I should have spent more time researching how to do this the physics way. As discussed with Zafar, a good physics implementation would have a lot of merits, as everything should just work in theory.

The question remains up for discussion, on whether we should stick with our current implementation, or try a physics-based implementation instead, and whether this is worth the time, if the benefits outweigh the time required and whether we think we can do it well. This is a discussion we should have sooner rather than later, and I will aim to bring it up in the near future within the team.

Implementing jump

I struggled quite a bit when I started doing this. The question that always needs to be answered is Can I jump?. The player can jump when they’re on the ground, but how do we know this? The typical approach is to have a ground sensor fixture on the player, and a boolean flag indicating that the player is grounded when the sensor collides with something, and indicating that the player is in-air when the sensor leaves collision.

This is straightforward enough, and can be implemented in the BeginContact and EndContact functions of the physics engine. The problem, however, is that it does not work. I found out the reason for that by doing some more research, where I stumbled upon this article titled Box2D C++ tutorials: the ‘can I jump’ question (IForce2D, 2011).

What happens in Box2D is that polygon shapes might be made up of several fixtures, if the shape is concave. After all, you can’t have concave polygons (at least not in OpenGL, as I was told in my studies). Furthermore, if the ground is made up of tiles, the player could be colliding with more than one of them at a time (for example, standing in a gap between two tiles). Therefore, a boolean flag gets out of sync pretty quickly.

As the article suggests, I implemented a collision counter instead, to tackle this issue:

void CGCObjPlayer::IncrementGroundCollisions()
{
	//just touched the ground, reset appropriate variables
	if( 0 == m_uNoGroundCollisions )
	{
		//want to be able to dash as soon as we reach the ground
		m_fDashCooldownTimer	= m_fDashCooldownLimit;
		//reset ability to dash while jumping
		m_bHasJumpDashed		= false;

		//check if the player is still jumping and disable jump if so
		m_uJumpCooldown			= 0;
	}

	m_uNoGroundCollisions++;
}

void CGCObjPlayer::DecrementGroundCollisions()
{
	m_uNoGroundCollisions--;
}

If the number of player ground sensor collisions is 0 and we enter a collision, this means we have just touched the ground somewhere. That’s when we want to execute our logic related to this. If the ground sensor is decremented to 0 on EndContact, this means that the player is in-air, and we can set the appropriate flags then.

This solved my problem pretty well and made the jump work just as expected.

Timers everywhere

When dealing with a physics engine, there are a lot of edge cases I needed to consider, which manifest themselves as bugs. For example, a player might jump towards a platform and only their ground sensor would collide with the edge of the platform. In which case, jumping again is enabled immediately, and the player can jump again, adding their previous vertical velocity to the new velocity of the jump and thus jumping twice the amount.

This was fixed by inserting a timer for how long the player needs to be on the ground before they can jump again, so that the protruding ground sensor would not be the only indicator of when the player is definitely on the ground.

const u32 k_uJumpCooldownLimit = 5;
u32 m_uJumpCooldown;

Note that this first timer was done in terms of frames passed and not the actual time passed. For a timer that does not require that much precision, this seems to work fine, but refactoring it would still be a sensible idea.

Another special case is when the player is dashing. You don’t want the player to do anything else when they are doing that, so I put a timer for the duration of the dash to disable all input.

f32 m_fDashTimer;
f32 m_fDashLength;

A further timer is needed to prevent the player from dashing indefinitely, so there needed to be some sort of “cooldown” between dashes.

f32 m_fDashCooldownTimer;
f32 m_fDashCooldownLimit;

There are a lot of cases where timers seem like the easiest solution. However, they do not look that great from an implementation perspective, as you need to initialise the timer, update it, reset it and use it to enable/disable things from time to time. It is relatively cheap to implement a timer, which I think is good in retrospect. However, as the need for more and more timers grows, I feel like they might need to be factored out into some sort of class which manages all of the timers collectively and the programmer can query each timer when the need arises without being bothered too much about maintaining them. This is something I will need to look into going forward, especially if the amount of timers keeps increasing more and more.

Dash mechanic

When the player performs a dash, an important question to consider is what happens when they finish dashing. This is more of a design decision, but it needs to be implemented as well. From our discussions with the team, we decided that when the player starts dashing, their pre-dash velocity is backed-up, and then restored when they finish dashing:

//start of dash
if( false == IsDashing() )
{
	//back up velocity
	m_v2PreDashVelocity			 = GetPhysicsBody()->GetLinearVelocity();
	m_v2PreDashVelocity.y		 = 0.0f;
	//reset the timer
	m_fDashTimer				 = 0.0f;
	...
}

...

//if we want to stop dashing now
if( m_fDashTimer >= m_fDashLength )
{
	m_bDashing = false;
	//check the facing direction so that we restore velocity in the correct direction
	m_v2PreDashVelocity.x = fabs( m_v2PreDashVelocity.x ) * m_fFacingDirection;

	//restore velocity
	if( 0.0f == m_v2PreDashVelocity.x )
	{
		m_v2PreDashVelocity.x = ( m_fFacingDirection * m_fTopSpeedX ) / 2.0f;
	}

	GetPhysicsBody()->SetLinearVelocity( m_v2PreDashVelocity );

}

One bug that arose from this was when the player would jump in one direction, turn around mid-air and instantly start dashing. At the end of the dash, the player would be propelled backwards as per their previous velocity.

This meant that a character facing direction variable needed to be introduced, which would be used in all cases when the player’s current facing direction is important to determine velocities.

Jumping through platforms

This was something that Alvaro worked on for a while, and then we started working on it collectively. The goal is for the player to be able to jump onto platforms from underneath them. I am sure Alvaro would expand on his approach to this problem more, but the basic gist of it was to have a head sensor for the player to track when they enter a platform from underneath, and a ground sensor to track when they exit the platform.

The problem that arose from this is that it does not produce consistent behaviour due to tiling and concave polygons forming several fixtures. Because a head sensor contacted several fixtures, does not mean that the ground sensor will as well, due to the angled jumps that the player can perform.

We had to stop and explore other approaches to this, including this one: http://www.iforce2d.net/b2dtut/one-way-walls . However, it did not work as well, since our character had a fixed rotation and the code suggests modifying the Box2D source, which has huge implications to how we handle collisions so far.

We then turned to an alternative approach, which is comparing the positions of the bottom of the player sprite with the top of the platform sprite. This finally worked:

bool CGCGameLayerPlatformer::CheckObjectAboveAnotherObject( const CGCObjSpritePhysics* rcObj1, const CGCObjSpritePhysics* rcObj2 ) const
{
	//get the position of the lower left corner of object 1
	CCPoint v2Obj1LocalPos = rcObj1->GetSprite()->getPosition();
	CCPoint v2Obj1WorldPos = rcObj1->GetSprite()->convertToWorldSpace( v2Obj1LocalPos );

	//get the position of the upper left corner of object 2
	CCPoint v2Obj2LocalPos = rcObj2->GetSprite()->getPosition();
	v2Obj2LocalPos.y += rcObj2->GetSprite()->getContentSize().height;
	CCPoint v2Obj2WorldPos = rcObj2->GetSprite()->convertToWorldSpace( v2Obj2LocalPos );


	if( v2Obj2WorldPos.y > v2Obj1WorldPos.y )
	{
		return false;
	}
	else
	{
		return true;
	}
}

And it was consistent as well. Furthermore, it extended to other objects such as the pushable block, so it was refactored into a helper function.

While it would probably be a bad idea to use sprite positions for this, as their sizes may change, we are not actually relying on the sizes and just compare relative positions. This does not have any side-effects related to scaling or resizing sprites.

The only implication that this has is that the platforms have to be flat, with no foreground or background elements protruding from them. However, I think this is fine, since we do want our platforms to be flat and we have agreed to this with the art team.

One more time

If I were to do the controller again, I would aim to implement it in a physics-based way. I might get stuck and frustrated, but I think it would be worth a try with more research into doing this. The only benefit of the current control implementation is that the controls are tight and easily tunable, but if this is possible to achieve with physics, then that benefit fades away.

However, the big downside of such an implementation is that it conflicts with the calculations that Box2D does, hence adding manual labour to actively bypass the physics engine which should be helping development, not slowing it down.

Related source files

GCObjPlayer.h – https://drive.google.com/open?id=0B_BnvnFZLH7mU0JoQ24yZmRsOVE

GCObjPlayer.cpp – https://drive.google.com/open?id=0B_BnvnFZLH7mR3ExNmljYmZrVGs

GCGameLayerPlatformer.cpp – https://drive.google.com/open?id=0B_BnvnFZLH7mN1hlZUZQdlVUU28