TGB/CustomPlatformer

From TDN

{{Template:ContentStandard|

Contents

Introduction

Welcome!

Firstly, if you would like to comment on this tutorial, please use the Discussion page, or visit the forum thread.

If you would like to add to this tutorial, could you please leave a note telling the world of your contribution? Wiki is an incredible tool, and everyone should know if you’ve done something good (or bad).

I would, however, ask that you only make modifications if you are completely sure that they will improve what I have put here. I don’t want hack jobs and mini-fixes to minor errors. I speak of “castCollision” and alike. While these kinds of functions may work some of the time, they are (usually) implemented poorly and often give undesirable results.

If you feel strongly about adding something like the above, please put it in the “Possible Fixes to Errors Above” section. This allows users to judge for themselves whether or not to implement your code.

Finally, if you find a bug not listed, could you please either post it in the forum thread, leave it on the Discussion page, or put it on the error list at the bottom.

Please work through this step-by-step, a lot of thought as gone into what I have written here. And while it is not perfect, it is a great improvement to some of the other basis available.

Note: I've just updated this tutorial, it may have errors in it because I have not checked it. I will check it as soon as I have time. This note will be removed when I have reviewed the code.

Setup

Note: I am assuming you have followed the MiniPlatformerTutorial and that you understand some of the issues that TGB has with the Platformer Genre. I will not cover the basics, nor teach you how to set the scene up. If you are unsure of how to setup your project, I advise that you check out the MiniPlatformerTutorial to get an idea of what we are doing here.

I would like you to set your scene up similar to MiniPlatformer’s setup. However, do not enable collisions on your tile layer. We are going to use a custom collision system involving the “t2dSceneObject” object.

Since we are using TGB 1.5, we will be taking advantage of the Behavior system. It is fundamentally a template system which allows us to add behaviors to specific objects.

Creating our Actor

You should have dragged out the “playerStand” animation onto the scene.

Select him, click “Edit”, then in the “Behaviors” rollout you should see a drop down menu, if you click this, there will not be anything listed there.

Behaviors are called automatically from the “game/behaviors” directory within your project’s folder. Browse to the behaviors folder and create the file “ActorBehavior.cs” and open it up.

Note: If you do not know where your project is located, the default path is “Documents/My Games/project_name/game/behaviors”.

if (!isObject(ActorBehavior))
{
	%template = new BehaviorTemplate(ActorBehavior);
   
	%template.friendlyName = "Actor Object";
	%template.behaviorType = "Actor";
	%template.description  = "Player object handling movement and physics";

	%template.addBehaviorField(upKey, "Keybind Up", keybind, "keyboard up");
	%template.addBehaviorField(downKey, "Keybind Down", keybind, "keyboard down");
	%template.addBehaviorField(leftKey, "Keybind Left", keybind, "keyboard left");
	%template.addBehaviorField(rightKey, "Keybind Right", keybind, "keyboard right");
	%template.addBehaviorField(jumpKey, "Keybind Jump", keybind, "keyboard space");

	%template.addBehaviorField(Accel_X, "X-Axis acceleration", float, 3);
	%template.addBehaviorField(Decel_X, "X-Axis deceleration", float, 2);

	%template.addBehaviorField(MaxVelocity_X, "X-Axis max velocity", int, 20);
	%template.addBehaviorField(MaxVelocity_Y, "Y-Axis max velocity", int, 60);

	%template.addBehaviorField(ConstantForce_X, "X-Axis gravitic force", int, 0);
	%template.addBehaviorField(ConstantForce_Y, "Y-Axis gravitic force", int, 18);
}

Copy this code into the “ActorBehavior.cs” file and save it. To save on tedious repetition, assume that all code blocks must be inserted into the “ActorBehavior.cs” file, unless specified otherwise.

This block of code will create the new behavior called ActorBehavior. Each of the “addBehaviorField” function calls will create user controlled variables editable from the TGB editor. Default value can be overwritten through the TGB editor, so you will not have to modify the above.

Once you have saved the file, click the “Project” menu and then “Reload Project”. This will refresh everything you have and load the newly added behavior.

Upon reloading the project, select the “playerStand” animation you had placed earlier and add the new behavior (remember to click the plus button). If you have done this correctly, you will see the following:

Image:CPlatformer_ActorBehavoir.jpg

Note: If you cannot see the behavior in the list you must have done something wrong. For starters, ensure that the “ActorBehavior.cs” file is within the “game/behaviors” directory.

Actor Movement

Quite a lengthy discussion has been made about Platformer physics and movement in the forums (specifically http://www.garagegames.com/mg/forums/result.thread.php?qt=51779). I advise that you read up on some of the issues that TGB itself has with the Platformer style game. It will aid you in fixing any problems that you have, or will potentially have.

In this section, we are going to start moving our actor. The MiniPlatformerTutorial covers movement well, but I am going to take it one step further. Upon pressing a directional button, our actor will accelerate up to the maximum velocity specified by the user. When the directional button is released, the actor will decelerate to 0. It is a pretty simple concept, but identifies another problem for TGB’s collision system.

If we applied the same physics that the MiniPlatformerTutorial does, we would have the actor oscillating on the contact point. TGB, upon collision, will attempt to clamp the colliding object to the destination object. If we’re repeatedly attempting to move into the destination object, we will be jerked out, giving an oscillating effect.

To begin, we’re going to add a few defaults to our actor.

function ActorBehavior::onAddToScene(%this, %scenegraph)
{
	%this.owner.enableUpdateCallback();

	%this.owner.setCollisionActive(1,1);
	%this.owner.setCollisionCallback(1);

	%this.owner.setCollisionPhysics(0,0);

	%this.Velocity_X = 0;
	%this.Velocity_Y = 0;

	if (!isObject(moveMap))
		return;

	moveMap.bindCmd(getWord(%this.leftKey, 0), getWord(%this.leftKey, 1),  %this @ ".moveLeft();",  %this @ ".stopLeft();");
	moveMap.bindCmd(getWord(%this.rightKey, 0), getWord(%this.rightKey, 1), %this @ ".moveRight();", %this @ ".stopRight();");
	moveMap.bindCmd(getWord(%this.jumpKey, 0), getWord(%this.jumpKey, 1), %this @ ".moveJump();", "");
}

When the behavior is called from an object, it will run this code. Notice that we are turning off all TGB physics at this point? We are going to handle collisions ourselves via a custom “onCollisionCallback” function.

function ActorBehavior::moveLeft(%this)
{
	if (!%this.owner.getFlipX())
		%this.owner.setFlipX(1);

	%this.moveLeft = true;
	%this.moveRight = false;
}

function ActorBehavior::moveRight(%this)
{
	if (%this.owner.getFlipX())
		%this.owner.setFlipX(0);

	%this.moveLeft = false;
	%this.moveRight = true;
}
function ActorBehavior::stopLeft(%this)
{
	%this.moveLeft = false;
}

function ActorBehavior::stopRight(%this)
{
	%this.moveRight = false;
}

function ActorBehavior::moveJump(%this)
{
	if (%this.owner.onGround)
	{
		%this.owner.setLinearVelocityY(-%this.MaxVelocity_Y);
	}
}

The above is fairly simple. When there is a key press (or lack of), it will call the above functions. These will not move the player directly, just flag a movement state. Movement will be handled through the “onUpdate” call-back, which was enabled above.

function ActorBehavior::onUpdate(%this)
{
	%this.updateHorizontal();
}

function ActorBehavior::updateHorizontal(%this)
{
	if (%this.moveLeft) {
		%dir = -1;
		%this.Velocity_X -= %this.Accel_X;
	}
	else if (%this.moveRight)
	{
		%dir = 1;
		%this.Velocity_X += %this.Accel_X;
	}
	else
	{
		%dir = -1;

		if (%this.Velocity_X > 0)
			%dir = 1;

		if (mAbs(%this.Velocity_X) > mAbs(%this.Decel_X))
			%this.Velocity_X -= %dir*%this.Decel_X;
		else
			%this.Velocity_X = 0;
	}

	if (mAbs(%this.Velocity_X) > mAbs(%this.MaxVelocity_X))
		%this.Velocity_X = %dir*%this.MaxVelocity_X;

	%this.owner.setLinearVelocityX(%this.Velocity_X);
} 

Okay, the first function will be called when the scene updates (which is every 32ms, or 0.032 seconds). This will call “updateHorizontal” and then move our player around.

“updateHorizontal” should fairly easy to understand, though I will walk you through it.

If there is a key press increase velocity in that direction by the acceleration amount, otherwise decelerate it by the deceleration amount. This will give us a nice fluid movement vector for our actor.

What’s happening? Say we’re moving in the right direction at a velocity of 40. We then press the left button, causing us to decelerate 40 through to -40, by the specified acceleration amount.

When you are creating these sorts of things, I want you to think logically. I don’t want you to just copy and paste the code into your text editor and run it, because you won’t have learnt anything. One thing I like to do when coding is have a scrap book and pen handy. If I have problems, I draw diagrams of what *should* happen, then work through it step-by-step. Moving on.

If you release the button, without attempting to move in the opposite direction, you will decelerate by the amount that was specified earlier.

We then ensure that we’re not travelling faster than what we should be travelling, then apply the velocity.

This sums up basic movement.

Save and run. If all goes well, you should be able to move the actor left and right, accelerating and decelerating nicely.

Note: We have not enabled gravity, or collisions. All you can do is move left and right.

Collision Setup

Before we actually apply any real Platformer functionality to our project, we need to set up the actor’s collision bounds and platforms themselves.

Select our actor animation and click “Edit this object collision polygon”, modifying it to something like below:

Note: The more points you have on your polygon, the more time it takes to detect collisions. This might not be problematic with a small project, but with large projects, minimalization is important.

Image:CPlatformer_ActorPolygon.jpg

Now that we have this set up, we would like to have a record of the collision bounds stored to be used with our collision system.

function ActorBehavior::onAddToScene(%this, %scenegraph)
{
	...
	%this.owner.collisionBounds = %this.getCollisionBounds();
	...
}

Add the above line of code to the “onAddToScene” function added earlier.

function ActorBehavior::getCollisionBounds(%this)
{
	%polylist = %this.owner.getCollisionPoly();
	%polyCount = getWordCount( %polylist ) / 2;

	%firstX = getWord( %polyList, 0 );
	%firstY = getWord( %polyList, 1 );

	%polyMinX = %firstX;
	%polyMaxX = %firstX;
	%polyMinY = %firstY;
	%polyMaxY = %firstY;

	for( %i = 1; %i < %polyCount; %i++ )
	{
		%thisX = getWord( %polyList, %i * 2 );
		%thisY = getWord( %polyList, ( %i * 2 ) + 1 );

		if( %thisX > %polyMaxX )
			%polyMaxX = %thisX;
		else if( %thisX < %polyMinX )
			%polyMinX = %thisX;

		if( %thisY > %polyMaxY )
			%polyMaxY = %thisY;
		else if( %thisY < %polyMinY )
			%polyMinY = %thisY;
	}

	return %polyMinX SPC %polyMinY SPC %polyMaxX SPC %polyMaxY;
}

This function will just get the maximum and minimum values of the collision polygon (essentially giving us a square bounding box instead of a polygon). For visual representation, check out the red line below:

Image:CPlatformer_ActorBounds.jpg

I would like to say thank you to Thomas Buscaglia for the above code and the “updateOnGround” function (to be added soon).

Platforms (Solid and One-Way)

To keep things nice and customizable, we’re going to use blank scene objects (t2dObject) and behaviors to handle platforms themselves.

Before creating the behavior itself, drag out a blank Scene Object from the bottom of the “Create” panel. Place it on top of an area that you wish the player to collide with, and resize accordingly.

Check out the picture below:

Note: The blank scene objects are sitting on top of the tile layer with a light blue border.

Image:CPlatformer_GameStage.jpg

Open up the “game/behaviors” folder and add a new file “PlatformBehavior.cs”. Open it up and paste the following code into it.

if (!isObject(PlatformBehavior))
{
	%template = new BehaviorTemplate(PlatformBehavior);

	%template.friendlyName = "Platform Object";
	%template.behaviorType = "Platform";
	%template.description  = "Platform object";

	%template.addBehaviorField(OneWay, "One-Way Platform", bool, 0);
}

function PlatformBehavior::onAddToScene(%this, %scenegraph)
{
	%this.owner.setCollisionActive(0,1);
	%this.owner.setCollisionCallback(0);

	%this.owner.setCollisionPhysics(0,0);
	%this.owner.isGround = true;

	%this.owner.isOneWay = %this.OneWay;
}

The above should be familiar to you. We’re doing exactly what we did for the actor behavior, adding the template and creating some default values.

Reload the project.

Select a platform (the t2dObject, not t2dTileLayer) and add the “Platform” behavior to it. Do this for each of the scene objects you added before and make a few of them OneWay by checking the check box.

Collision Handling

We are going to introduce four seemingly simple tasks in this section.

  1. Gravity
  2. Collision Handling
  3. Grounding Detection
  4. Wall Detection

These *should* be fairly simple tasks to employ, yet it can be quite difficult to do correctly. I have tested this method quite thoroughly, but I am sure there are still a few flaws so please test it out and let me know what’s up!

Adding Gravity

function ActorBehavior::onUpdate(%this)
{
	...
	if (!%this.owner.onGround)
		%this.owner.setConstantForceY(%this.ConstantForce_Y);
	...
}

Add this to the “onUpdate” function added earlier.

What does it do you ask? If we’re not on the ground, push us towards it.

Collision Handling

function ActorBehavior::onCollision(%srcObject, %dstObject, %srcRef, %dstRef, %time, %normal, %contacts, %points)
{
	if (!%dstObject.isGround)
		return;

	if (getWord(%normal, 0) != 0  && !%dstObject.isOneWay)
	{
		if (getWord(%normal, 0) > 0)
		{
			%dir = 1;
			if (%srcObject.owner.getFlipX())
				%collisionBounds_X = mAbs(getWord(%srcObject.owner.collisionBounds, 2));
			else
				%collisionBounds_X = mAbs(getWord(%srcObject.owner.collisionBounds, 0));
		}
		else
		{
			%dir = -1;
			if (%srcObject.owner.getFlipX())
				%collisionBounds_X = mAbs(getWord(%srcObject.owner.collisionBounds, 0));
			else
				%collisionBounds_X = mAbs(getWord(%srcObject.owner.collisionBounds, 2));
		}

		%point_X = getWord(%points, 0) + %dir*(0.01+%collisionBounds_X)*(%srcObject.owner.getSizeX()/2);

		%srcObject.Velocity_X = 0;
		%srcObject.owner.setLinearVelocityX(0);

		%srcObject.owner.setPositionX(%point_X);
	}

	if (getWord(%normal, 1) < 0 && %srcObject.owner.getLinearVelocityY() > 0)
	{
		%srcBottom = getWord(%srcObject.owner.getWorldPoint(0, getWord(%srcObject.owner.collisionBounds,3)),1);
		%dstTop = getWord(%dstObject.getWorldPoint(0, -1),1);

		if (%dstTop > %srcBottom || !%dstObject.isOneWay)
		{
			%point_Y = getWord(%points, 1) - (0.01+getWord(%srcObject.owner.collisionBounds,3)*%srcObject.owner.getSizeY()/2);

			%srcObject.owner.setLinearVelocityY(0);
			%srcObject.owner.setConstantForceY(0);
			%srcObject.owner.setPositionY(%point_Y);
		}
	}

	if (getWord(%normal, 1) > 0 && !%dstObject.isOneWay)
		%srcObject.owner.setLinearVelocityY(0);
} 

Okay, confusing right?

The first bit makes sure we’re colliding with a platform. If colliding with more than platforms, I suggest that you do a check and link to another function. For example (please don’t add, it’s just an idea!):

	if (%dstObject.isGround)
		%srcObject.handleGroundCollision(%normal, %points);

	If (%dstObject.isEnemy)
		%srcObject.handleEnemyCollision(%normal, %points);

The second series of statements handle x-axis collisions.

If we have are colliding to the left hand side, ensure that the collision bounds we use are the maximum values (which correspond to the right hand side, but we use them because we’re flipped, right?). If we’re colliding on the right, use the maximum bounds as well.

Note: I’ve left the minimum bounds in there too, just in case we get pushing into a wall when we’re not facing it.

After we’ve sorted out what side we’re on, we stop x-axis velocity and clamp it to the side of the destination object.

Note: Originally, I wasn’t using the “%points” variable, because I assumed that everything was square, but this method allows us to use ramps (to be added later).

	%point_X = getWord(%points, 0) + %dir*(0.01+%collisionBounds_X)*(%srcObject.owner.getSizeX()/2);

Above, you will notice that I am getting the point of the collision and offsetting the actor based on the collision bounds. What I am also doing is offsetting the collision by an extra 0.01 units. Although the actor will be perfectly placed beside the colliding object, the “onCollision” function will continue to be called.

Why is this problematic? Multiple “onCollision” functions will not be called in a frame, and if it were to be called, there would be a huge tax on resources. We want to prevent this from occurring to save on resources, and fix a few minor bugs that might pop up.

As we did for the x-axis, we clamp the actor to the collision point and offset it by his height (and the 0.01 unit buffer). We then kill the velocity ensuring we don’t keep falling down.

This sums up x-axis collisions.

The y-axis collision group acts in a very similar manor. If we’re colliding downwards, check to see if we’re actually above the object and not inside it and then clamp us to the top. I’ve included the same offset that I have used in the x-axis collisions. It will give us the same results as above, moving us just outside of collision detection range, while giving us the feeling of being on ground.

If at this point you have run the game, you will notice that you cannot jump. Also, you will muscle your way into a wall. These two problems are not good, so we must fix them.

Ground Detection

function ActorBehavior::onUpdate(%this)
{
	%onGround = %this.isOnGround();
	...
} 

Ensure that you put the above amendment at the top of the “onUpdate” function.

function ActorBehavior::isOnGround(%this)
{
	if(%this.owner.getConstantForceY() != 0)
	{
		%this.owner.onGround = false;
		%this.owner.groundSurface = "";
		return false;
	}

	%maxY = getWord(%this.owner.collisionBounds, 3);
	%minX = getWord(%this.owner.collisionBounds, 0);
	%maxX = getWord(%this.owner.collisionBounds, 2);

	if(%this.owner.getFlipX())
	{
		%minX = -%minX;
		%maxX = -%maxX;
	}

	%leftBottom = %this.owner.getWorldPoint(%minX, %maxY);
	%rightBottom = %this.owner.getWorldPoint(%maxX, %maxY);

	%minX = getWord(%leftBottom, 0);
	%maxX = getWord(%rightBottom, 0);
	%maxY = getWord(%leftBottom, 1) + 0.1;

	%edgeWidth = (%maxX - %minX) / 10;
	%minX += %edgeWidth;
	%maxX -= %edgeWidth;

	%leftBottom = %minX SPC %maxY;
	%rightBottom = %maxX SPC %maxY;

	%objectList = %this.owner.getSceneGraph().pickLine(%leftBottom, %rightBottom, -1, -1, false, %this.owner);

	%count = getWordCount(%objectList);
	for(%i=0; %i<%count; %i++)
	{
		%currObject = getWord(%objectList, %i);
		if (isObject(%currObject) && %currObject.isGround)
		{
			%this.owner.onGround = true;
			%this.owner.groundSurface = %currObject;

			if (!%this.owner.hitWall)
				%this.owner.groundNormal = %currObject.groundNormal;

			return true;
		}
	}

	%this.owner.onGround = false;
	%this.owner.groundSurface = "";
	return false;
} 

This function will find the bounds of the actor and draw a line across its feet (with an offset of 0.1). This “pickling” will find a list of all objects within that line, enabling us to then search the list to find a platform. If there is a platform under our toes, we’ll tell the program that we’re on the ground, and then return.

This will solve our problem of muscling our way into the wall. This is because our actor had constant force constantly imposed upon him, forcing him downwards. When this happens, he unfortunately ignored the ground underneath him.

Save and run.

Wall Detection

All of the above has helped prevent unnecessary y-axis collisions, and set us up for a nice neat x-axis system, but we need to finish it off.

function ActorBehavior::onUpdate(%this)
{
	%onGround = %this.isOnGround();
	%hitWall = %this.checkSide();
	...
}
function ActorBehavior::checkSide(%this)
{
	%minY = getWord(%this.owner.collisionBounds, 1);
	%maxX = getWord(%this.owner.collisionBounds, 2);
	%maxY = getWord(%this.owner.collisionBounds, 3);

	%dir = 1;
	if (%this.owner.getFlipX())
	{
		%dir = -1;
		%maxX = -%maxX;
	}

	%minRight = %this.owner.getWorldPoint(%maxX, %minY);
	%maxRight = %this.owner.getWorldPoint(%maxX, %maxY);

	%upper = getWord(%minRight, 1);
	%lower = getWord(%maxRight, 1);
	%right = getWord(%minRight, 0) + %dir * 0.1;

	%upper = %right SPC %upper;
	%lower = %right SPC %lower;

	%objectList = %this.owner.getSceneGraph().pickLine(%upper, %lower, -1, -1, false, %this.owner);

	%count = getWordCount(%objectList);
	for(%i=0; %i<%count; %i++)
	{
		%currObject = getWord(%objectList, %i);
		if (isObject(%currObject) && %currObject.isGround && !%currObject.isOneWay)
		{
			%this.Velocity_X = 0;
			%this.owner.setLinearVelocityX(0);

			%this.owner.hitWall = true;
			%this.owner.hitWallSurface = %currectObj;

			if (%currObject.isOneWay)
				%this.owner.hitWallDir = 0;
			else
				%this.owner.hitWallDir = %dir;

			return true;
		}
	}
	
	%this.owner.hitWall = false;
	%this.owner.hitWallSurface = "";
	%this.owner.hitWallDir = 0;
	return false;
}

Same thing as “isOnGround”, except we’re drawing a line along the right hand side of the actor (flipping it to the left hand side of we’re on moving left).

This works a treat in detecting if a wall is there, except it’s not helping us prevent collisions on the side. You need to change the “if” statements in the “updateHorizontal” function.

	...
	if (%this.moveLeft && %this.owner.hitWallDir != -1) {
		%dir = -1;
		%this.Velocity_X -= %this.Accel_X;
	}
	else if (%this.moveRight && %this.owner.hitWallDir != 1)
	{
		%dir = 1;
		%this.Velocity_X += %this.Accel_X;
	}
	...

This will prevent us from moving forwards if we’re going to hit a wall. It also aids in preventing multiple “onCollision” function calls.

Polishing Up

I have been talking about the use of resources and how to effectively use them, but in the above two functions, I am going to be using a fair bit. This is why we only want to call them when we have to. Your “onUpdate” function should be modified to look like below.

function ActorBehavior::onUpdate(%this)
{
	if (%this.currentPosition !$= %this.owner.getPosition())
	{
		%this.currentPosition = %this.owner.getPosition();

		%onGround = %this.isOnGround();
		%hitWall = %this.checkSide();
	}

	%this.updateHorizontal();

	if (!%this.owner.onGround)
		%this.owner.setConstantForceY(%this.ConstantForce_Y);
}

Moving Platforms and Ramps

At this point, we have assumed that the object we’re colliding with below us is a flat surface and that it is not moving at all. One of these solutions is easy to rectify, the other is very difficult (I’ll leave you to guess which is tough at this stage).

I would like to point out that the implementations of the solutions to these problems are about 90% efficient. There are a few problems that I have encountered, ones that are frustrating indeed. More will be discussed on these matters later, however.

We will be implementing both moving platforms and ramps in one foul swoop, but first, I will discuss our problems.

Key Issues

When we move sideways on a flat surface, we only have to introduce x-axis velocity. When we move sideways on a slopped surface, we have to introduce x-axis as well as y-axis velocity. The amount of y-axis movement versus x-axis movement depends on the angle that the slope makes. If you observe our “onCollision” function, you will notice a variable called “%normal”. This is a unit vector (a vector with a distance of 1 unit, correct me if I am wrong), which gives us the information we need to employ, but we’re not going to use it ;)

The problem we have is that if we collide with an object to find the normal vector, it is often too late. We need to know the normal vector of the points in front of us (or behind if we’re on a slope facing down) and this poses a problem.

Okay, so, we can sort out that issue, but another interesting one arises once we have that information. If we’re on a slope, moving upwards, how do we know we’re not on a slope anymore? This is the part that has given me the most trouble. If we’re at the peak of the slope, we need to find the flat part of the surface in front of us before we move off the ground. I have found that my implementation works great if we are moving relatively slowly, but if we’re moving quickly up the ramp, we will lift off the ground for approximately 3 frames. If we’re animating the player, we would see the jump animation being played, which isn’t really appropriate.

So, these are my (and now your) problems, let us try to solve them.

Ramps

Image:CPlatformer_MiniRamp.png

Grab the above, it is our ramp. Just drag out the sprite onto your scene (don’t use it as a tile, otherwise you won’t have a background). Create a little series of ramps or something and add the collision box on top just like we did for normal platforms.

Image:CPlatformer_Ramp.jpg

Note: To overcome one small issue I have found, create the collision polygon in the order above. I have not given preference to points, nor objects, so it finds the first points that satisfy a few conditions and returns the values. This will be sorted out when I update this tutorial next.

If you ran it as is, you won’t be able to move up and down the ramp, in fact, it may do a few weird things.

function ActorBehavior::isOnGround(%this)
{
	if(%this.owner.getConstantForceY() != 0)
	{
		%this.owner.onGround = false;
		%this.owner.groundSurface = "";
		return false;
	}

	%maxY = getWord(%this.owner.collisionBounds, 3);
	%minX = getWord(%this.owner.collisionBounds, 0);
	%maxX = getWord(%this.owner.collisionBounds, 2);

	if(%this.owner.getFlipX())
	{
		%minX = -%minX;
		%maxX = -%maxX;
	}

	%leftBottom = %this.owner.getWorldPoint(%minX, %maxY);
	%rightBottom = %this.owner.getWorldPoint(%maxX, %maxY);

	%minX = getWord(%leftBottom, 0);
	%maxX = getWord(%rightBottom, 0);
	%maxY = getWord(%leftBottom, 1) + 0.5;

	%leftBottom = %minX SPC %maxY;
	%rightBottom = %maxX SPC %maxY;

	%objectList = %this.owner.getSceneGraph().pickLine(%leftBottom, %rightBottom, -1, -1, false, %this.owner);

	%count = getWordCount(%objectList);
	for(%i=0; %i<%count; %i++)
	{
		%currObject = getWord(%objectList, %i);
		if (isObject(%currObject) && %currObject.isGround)
		{
			%this.getCollisionInfo(%currObject, %leftBottom, %rightBottom);
			
			if (mAbs(getWord(%this.owner.groundNormal, 0)) > 0 && mAbs(getWord(%this.owner.groundNormal, 0)) < 0.75)
			{
				if (getWord(%this.owner.groundNormal, 0) > 0)
				{
					%dir = 1;
					if (%this.owner.getFlipX())
						%collisionBounds_X = mAbs(getWord(%this.owner.collisionBounds, 2));
					else
						%collisionBounds_X = mAbs(getWord(%this.owner.collisionBounds, 0));
				}
				else
				{
					%dir = -1;
					if (%this.owner.getFlipX())
						%collisionBounds_X = mAbs(getWord(%this.owner.collisionBounds, 0));
					else
						%collisionBounds_X = mAbs(getWord(%this.owner.collisionBounds, 2));
				}
				
				%point_X = %this.owner.collisionPoint_X + %dir*(0.01+%collisionBounds_X)*(%this.owner.getSizeX()/2);
				%point_Y = %this.owner.collisionPoint_Y - (getWord(%this.owner.collisionBounds,3)*%this.owner.getSizeY()/2);
		
				%this.owner.setPosition(%point_X SPC %point_Y);
				intersect.setPosition(%this.owner.collisionPoint_X SPC %this.owner.collisionPoint_Y);
			}
			else
			{
				%point_Y = %this.owner.collisionPoint_Y - (getWord(%this.owner.collisionBounds,3)*%this.owner.getSizeY()/2);
				
				%this.owner.setPositionY(%point_Y);
			}
			
			%this.owner.setLinearVelocityY(%currObject.getLinearVelocityY());

			%this.owner.onGround = true;
			%this.owner.groundSurface = %currObject;

			return true;
		}
	}

	%this.owner.onGround = false;
	%this.owner.groundSurface = "";

	%this.owner.groundNormal = "0 -1";

	return false;
}

Delete your old “isOnGround” function and add the above instead.

function ActorBehavior::getCollisionInfo(%this, %groundObject, %minBottom, %maxBottom)
{
	%polyList = %groundObject.getCollisionPoly();
	%polyCount = getWordCount(%polyList)/2;

	%diffBottom_X = getWord(%maxBottom, 0) - getWord(%minBottom, 0);
	%diffBottom_Y = 0;

	%minGroundY = %groundObject.getPositionY();

	for (%i=0; %i<%polyCount; %i++)
	{
		%currPoint_X = getWord(%polyList, %i*2);
		%currPoint_Y = getWord(%polyList, %i*2+1);
		%nextPoint_X = getWord(%polyList, %i*2+2);
		%nextPoint_Y = getWord(%polyList, %i*2+3);

		%bottomParra = false;

		if (%i == %polyCount -1)
		{
			%nextPoint_X = getWord(%polyList, 0);
			%nextPoint_Y = getWord(%polyList, 1);
		}

		%currPoint = %groundObject.getWorldPoint(%currPoint_X, %currPoint_Y);
		%nextPoint = %groundObject.getWorldPoint(%nextPoint_X, %nextPoint_Y);

		%currPoint_X = getWord(%currPoint, 0);
		%currPoint_Y = getWord(%currPoint, 1);

		%nextPoint_X = getWord(%nextPoint, 0);
		%nextPoint_Y = getWord(%nextPoint, 1);

		if (%this.owner.getFlipX())
		{
			%testPoint_MinX = getWord(%maxBottom, 0);
			%testPoint_MaxX = getWord(%minBottom, 0);
		}
		else
		{
			%testPoint_MinX = getWord(%minBottom, 0);
			%testPoint_MaxX = getWord(%maxBottom, 0);
		}
		
		if (%testPoint_MinX < %currPoint_X || %testPoint_MinX > %nextPoint_X)
		{
			if (%testPoint_MaxX < %currPoint_X || %testPoint_MaxX > %nextPoint_X)
				continue;
		}

		%diffPoint_X = %nextPoint_X - %currPoint_X;
		%diffPoint_Y = %nextPoint_Y - %currPoint_Y;

		if (%diffBottom_Y*%diffPoint_X  == %diffBottom_X*%diffPoint_Y)
			%bottomParra = true;

		if (!%bottomParra)
		{
			%testPoint_a = (getWord(%minBottom, 1) - %currPoint_Y)*%diffPoint_X - (getWord(%minBottom, 0) - %currPoint_X)*%diffPoint_Y;
			%testPoint_a = mAbs(%testPoint_a/(%diffBottom_Y*%diffPoint_X - %diffBottom_X*%diffPoint_Y));

			%testPoint_b = (getWord(%minBottom, 1) - %currPoint_Y)*%diffBottom_X - (getWord(%minBottom, 0) - %currPoint_X)*%diffBottom_Y;
			%testPoint_b = mAbs(%testPoint_b/(%diffBottom_Y*%diffPoint_X - %diffBottom_X*%diffPoint_Y));

			if (%testPoint_a > 0 && %testPoint_a < 1 && %testPoint_b > 0 && %testPoint_b < 1)
			{
				%diffPoint_X = %diffPoint_X/(%groundObject.getSizeX()/2);
				%diffPoint_Y = %diffPoint_Y/(%groundObject.getSizeY()/2);

				%diffPoint_X = %diffPoint_X*(%groundObject.getSizeX()/%groundObject.getSizeY());

				%dir = 1;
				if (%nextPoint_Y < %currPoint_Y)
					%dir = -1;

				%hypot = mSqrt(mPow(%diffPoint_X,2)+mPow(%diffPoint_Y,2));

				%this.owner.groundNormal = %dir*mAbs(%diffPoint_Y)/%hypot SPC -mAbs(%diffPoint_X)/%hypot;

				%this.owner.collisionPoint_X = getWord(%minBottom, 0) + %testPoint_a*%diffBottom_X
					+ %dir*getWord(%this.owner.groundNormal, 1)*0.5;
				%this.owner.collisionPoint_Y = getWord(%minBottom, 1) + %testPoint_a*%diffBottom_Y – 0.5;

				return true;
			}
		}
		else
		{
			if (%currPoint_Y == %nextPoint_Y && %currPoint_Y < %minGroundY)
			{
				%this.owner.groundNormal = 0 SPC -1;
				
				%this.owner.collisionPoint_X = NULL;
				%this.owner.collisionPoint_Y = %currPoint_Y;
				
				return true;
			}
		}
	}
	
	%this.owner.collisionPoint_X = NULL;
	%this.owner.collisionPoint_Y = NULL;
	
	return false;
}

Ouch! This might be the biggest function we’ve added yet. I will go through it bit-by-bit.

The objective of this function, as you may have guess by the name, is to find the normal vector of the surface of an object in front of us. Initially, we’re just storing some of data about the object we’ve just found in our pickLine. Then for each set of two points, we’re going to test to see if our pickLine points are intersecting with the line that the two collision points make.

If the two lines (the pickLine and the line the points form) are parallel, we’ll skip it. If we aren’t within the x-bounds of the points, we’ll skip it. Otherwise, we’ll find the normal vector of the two points! Who remembers Pythagoras’ Theorem? Basically, this is how we’re going to calculate the normal vector.

We take the x and y point difference, ensuring to scale the x difference appropriately. Once we have that, we can find the hypotenuse of the triangle we just formed and create the normal vector!

Now that we have that, we need to incorporate it into our actor’s movement. Replace your ‘updateHorizontal’ function with the following:

function ActorBehavior::updateHorizontal(%this)
{
	if (%this.owner.onGround && (getWord(%this.owner.groundNormal,0) != 0 && getWord(%this.owner.groundNormal,1) != -1))
		if (mAbs(getWord(%this.owner.groundNormal,0)) < 0.75)
			%isSloped = true;
	else
		%isSloped = false;

	if ((%this.moveLeft && %this.owner.hitWallDir != -1) || (%this.moveLeft && %isSloped))
	{
		%dir = -1;
		%this.Velocity_X -= %this.Accel_X;
	}
	else if ((%this.moveRight && %this.owner.hitWallDir != 1) || (%this.moveRight && %isSloped))
	{
		%dir = 1;
		%this.Velocity_X += %this.Accel_X;
	}
	else
	{
		%dir = -1;

		if (%this.Velocity_X > 0)
			%dir = 1;

		if (mAbs(%this.Velocity_X) > mAbs(%this.Decel_X))
			%this.Velocity_X -= %dir*%this.Decel_X;
		else
			%this.Velocity_X = 0;
	}

	if (mAbs(%this.Velocity_X) > mAbs(%this.MaxVelocity_X))
		%this.Velocity_X = %dir*%this.MaxVelocity_X;

	if (%this.owner.onGround)
	{
		if (getWord(%this.owner.groundNormal, 0) > 0)
		{
			%moveVector_X = -getWord(%this.owner.groundNormal, 1)*%this.Velocity_X;
			%moveVector_Y = -getWord(%this.owner.groundNormal, 0)*%this.Velocity_X;
		}
		else
		{
			%moveVector_X = mAbs(getWord(%this.owner.groundNormal, 1))*%this.Velocity_X;
			%moveVector_Y = mAbs(getWord(%this.owner.groundNormal, 0))*%this.Velocity_X;
		}

		%this.owner.setLinearVelocity(%moveVector_X + %this.owner.groundSurface.getLinearVelocityX(),
			-%moveVector_Y  + %this.owner.groundSurface.getLinearVelocityY());
	}
	else
	{
		%this.owner.setLinearVelocityX(%this.Velocity_X);
	}
}

This is pretty much the same, only we check to see if we’re on a slope then apply necessary manipulation to the movement vector.

One more thing to do is to change the ‘onCollision’ function. Since we’ve assumed that the surface we contact with is flat, it will check the top points of the object. Now, however, we aren’t always hitting a flat surface, we need to check the point of contact.

Change:

...
if (%dstTop > %srcBottom || !%dstObject.isOneWay)
...

With:

...
if (%srcBottom < getWord(%points, 1))
...

Note: The method I have employed above, is not flawless! There are issues I know about, and I am fixing as I get deeper into the code, but there are a few issues I cannot resolve. In the next update, I will have box picking rather than two lines, and I will prioritise objects, not just choose the first one it checks.

To Come

I plan on removing the onGround and hitWall functions to be replaced by a checkBounds function, picking a rectangular box around the player.

If you have suggestions, please use the Discussion page, or the forum thread.

Thank you for reading!

Known Bugs

Possible Fixes to Errors Above