Chapter 3 – Making the pawn move to our will

February 22nd, 2010 Leave a comment Go to comments

Now we have everything in place to get started on the fun part, namely player movement. Most of the following code will be harder than before, get ready to be lost a bit. I will try to the best of my ability to explain what is going on.

 

Most of the work from now on will occur in the IsometricGamePlayerController. In this chapter we will explore states and try to understand how they work.

 

 

Using the mouse to zoom in/out

 

Before tackling more challenging topics, lets try something easy to understand : user inputs. All user inputs are stored in a file in <UDKFolder>\UTGame\Config\UTInput.ini. Should you like to map a function to a specific input name the engine understand, its here you are going to do it. For more information, a thread is available here. Lets first examine what a binding line looks like :

 

Bindings=(Name=”XboxTypeS_LeftY”,Command=”GBA_MoveForward_Gamepad”)

 

What this line is declaring is that when the user inputs XboxTypeS_LeftY (this should be obvious its for the XBOX controller inputs) execute the following code command named GBA_MoveForward_Gamepad. Now if you search for “GBA_MoveForward_Gamepad” you will not find it in the code at all anywhere. Now inspect PlayerInput.uc and you will find that there is some difference between what is exactly declared here and what function it is linked to in the code. This is engine based and beyond the scope of this tutorial, lets understand that there is some functions pretty obvious and other more obscure. For what interest us at the moment are how to scroll with the mouse wheel. It appears there is an entry for that :

 

Bindings=(Name=”MouseScrollUp”,Command=”GBA_PrevWeapon”)

Bindings=(Name=”MouseScrollDown”,Command=”GBA_NextWeapon”)

 

Search for GBA_PrevWeapon in UTInput.ini and you will see that it was an alias for another command, both function PrevWeapon and NextWeapon exist in basic Player Controller and control the weapon switching. Since we are making and Isometric game tutorial with zoom we will redefine those functions.

exec function NextWeapon()

{

`Log(“MouseScrollUp”);

PlayerCamera.FreeCamDistance += 64;

}

exec function PrevWeapon()

{

`Log(“MouseScrollUp”);

PlayerCamera.FreeCamDistance -= 64;

}

This will change the zoom we saw in the Camera in chapter 1, look it up if its too far now. Notice that there will not be interpolation (smoothing) in this tutorial, you will have to implement it yourself. Fantastic how that was easy. Now we will tackle the mouse click.

 

Setting up advanced click

 

Now we could simply use the function StartFire which is called when the user click on the mouse, but that would be too easy. What we will do here, since we will want free movement (i.e. Holding the mouse and the pawn follows in the direction) we will have to check for

 

Implementing the mouse down event

 

For those of you knowledgeable about the mouse down function, it is not implemented by the engine so we have to do it ourselves with a little timer. Copy the code and read, compile later when I instruct you to, this part is rather long.

 

Declare a float variable that will be accumulating delta time between each frame. What is Delta time ? Its the elapsed time since last frame in float value. This is very handy for interpolating values and doing nifty maths over multiples frames. But since its a time value we can add it up to know how much time has passed.

var float DeltaTimeAccumulated; //Accumulate time to check for mouse clicks

we also need to know which mouse button was pushed :

var bool bLeftMousePressed; //Initialize this function in StartFire and off in StopFire

var bool bRightMousePressed; //Initialize this function in StartFire and off in StopFire

To accumulate properly time we need to do it each frame. What better function than PlayerTick ?

/******************************************************************
 *
 *  TUTORIAL FUNCTION
 *
 *  PlayerTick is called each frames.
 *
 ******************************************************************/
event PlayerTick( float DeltaTime )
{
	super.PlayerTick(DeltaTime);

	//Set the location of the 3d marker that moves with the mouse.
	MouseCursor.SetLocation(MouseHitWorldLocation);	

	//We use the right mouse button to move, change it to suit your need !
	if(bRightMousePressed)
	{
		//accumulate the time for knowing how much time the button was pressed.
		DeltaTimeAccumulated += DeltaTime;
	}
}

Movement will occur on right mouse press in this tutorial, change it after the tutorial if you wish but for the sake of following properly, keep it like this for now. When the Right mouse is held, we accumulate time. Thats all that is to take note for the PlayerTick for the moment.

To initiate this mouse held or mouse click we need to initialize all the variables inside the StartFire function.

/******************************************************************
 *
 *  TUTORIAL FUNCTION
 *
 *  StartFire is called on mouse pressed, here to calculate a mouse click we
 *  set the timer to 0, then initialize mouseButtons according to function
 *  parameter and set the initial destination of the mouse press. Real
 *  process is in PlayerTick function.
 *
 ******************************************************************/
exec function StartFire(optional byte FireModeNum)
{
	//Pop all states to get pawn in auto moving to mouse target location.
	PopState(true);

	//Set timer
	DeltaTimeAccumulated =0;

	//Set initial location of destination
	SetDestinationPosition(MouseHitWorldLocation);

	//Initialize this to false, so we can at least do one state-frame and evaluate distance again.
	bPawnNearDestination = false;

	//Initialize mouse pressed over time.
	bLeftMousePressed = FireModeNum == 0;
	bRightMousePressed = FireModeNum == 1;

	//comment these if not needed
	if(bLeftMousePressed) `Log("Left Mouse pressed");
	if(bRightMousePressed) `Log("Right Mouse pressed");
}

This function introduces an important new variables : bPawnNearDestination. You should add it now with the other variable on top of the file :

var bool bPawnNearDestination; //This indicates if pawn is within acceptable offset of destination to stop moving.

This variable will instruct our manual move states that the pawn is within acceptable range of the destination vector. We also need a variable to store remaining distance if you want to debug it to the hud at some point. For the moment we will only output to console, let it be your homework to plugin this variable on screen in real time when debug is activated (see chapter 2). Add the following close to bPawnNearDestination :

var float DistanceRemaining; //This is the calculated distance the pawn has left to get to MouseHitWorldLocation.

So now we are armed with a variable to let the pawn stop and one to calculate remaining distance. Lets define when the player releases the mouse button with StopFire :

/******************************************************************
 *
 *  TUTORIAL FUNCTION
 *
 *  StopFire is called on mouse release, here check the time the buttons have
 *  been pressed (this should be enhanced, but it was kept simple for the tutorial).
 *  if DeltaAccumulated < 0.1300 (medium time mouse click) then we calculate it as
 *  a mouse click, else simply stop any state running. EDIT: You must understand only
 *  a single timer has been kept for all mouse button, you should duplicate a timer
 *  for each individual mouse button if you want to support thing like auto-fire while
 *  walking in a direction.
 *
 ******************************************************************/
simulated function StopFire(optional byte FireModeNum )
{
	`Log("delta accumulated"@DeltaTimeAccumulated);
	//Un-Initialize mouse pressed over time.
	if(bLeftMousePressed && FireModeNum == 0)
	{
		bLeftMousePressed = false;
		`Log("Left Mouse released");
	}
	if(bRightMousePressed && FireModeNum == 1)
	{
		bRightMousePressed = false;
		`Log("Right Mouse released");
	}

	//If we are not near destination and click occured
	if(!bPawnNearDestination && DeltaTimeAccumulated < 0.13f)
	{
		//Our pawn has been ordered to a single location on mouse release.
		//Simulate a firing bullet. If it would be ok (clear sight) then we can move to and simply ignore pathfinding.
		if(FastTrace(MouseHitWorldLocation, PawnEyeLocation,, true))
		{
			//Simply move to destination.
			MovePawnToDestination();
		}
		else
		{
			//fire up pathfinding
			//ExecutePathFindMove();
		}
	}
	else
	{
		//Stop player from going on in that direction forever. This normally needs to be done
		//after a long mouse held. This will make the player stop its current MoveMousePressedAndHold
		//state.
		PopState();
	}
	//reset accumulated timer for mouse held button
	DeltaTimeAccumulated = 0;
}

Now the ExecutePathFindMove() has been commented out on purpose here. We will visit that later, its just for you to see the global picture. Lets talk about the first Log function here, remember at then end of chapter 2 we discussed the @ for concatenation. Here we output the content of the variable with some text for presentation.

We then proceed to initialization of mouse buttons. Then if we did not reach destination and time accumulated is smaller than an average click (change this to your liking), we try to move. First thing we do is check for obstacle using a fastTrace that returns true if nothing is between you and the destination. If something is blocking the pawns view to destination, we need the navigation algorithms.

Keep the PopState under the MoveMousePressedAndHold comment, it will be handy later, even if we get some warnings. At the end of mouse release, initialize the timer again. This could be commented since we initialize in StartFire, this is double protection.

We must declare the function MovePawnToDestination if we ever want this code to compile, lets add it now :

/******************************************************************
 *
 *  TUTORIAL FUNCTION
 *
 *  MovePawnToDestination will push a MoveMouseClick state that will make
 *  the pawn go to a single destination with a mouse click and then
 *  stop near the destination.
 *
 ******************************************************************/
function MovePawnToDestination()
{
	`Log("Moving to location without pathfinding!");
	SetDestinationPosition(MouseHitWorldLocation);
	PushState('MoveMouseClick');
}

This function only set the destinationPosition (Built-in controller function) and pushes our first state we will see in this tutorial :

MouseMoveClick

/******************************************************************
 *
 *  TUTORIAL STATE (MoveMouseClick)
 *
 *  MoveMouseClick is the state when a mouse button is pressed
 *  once (simple click). Simply go to a set destination.
 *
 *
 ******************************************************************/
state MoveMouseClick
{
	event PoppedState()
	{
		`Log("MoveMouseClick state popped, disabling StopLingering timer.");
		//Disable all active timers to stop lingering if they are active.
		if(IsTimerActive(nameof(StopLingering)))
		{
			ClearTimer(nameof(StopLingering));
		}
	}

	event PushedState()
	{
		//Set a function timer. If the pawn is stuck it will stop moving
		//by itself.
		SetTimer(3, false, nameof(StopLingering));
		if (Pawn != None)
		{
			// make sure the pawn physics are initialized
			Pawn.SetMovementPhysics();
		}
	}

Begin:
	while(!bPawnNearDestination)
	{
		`Log("Simple Move in progress");
		MoveTo(GetDestinationPosition());
	}
	`Log("MoveMouseClick: Pawn is near destination, go out of this state");
	PopState();
}

When the state is pushed into the state stack the function PushedState is called for the state. We set a timer here so that when the player is on auto-pilot, if he get stuck on a wall, he will stop jittering after 3 seconds. When the state pops, the PopState event will be called and we disable the timer. What the state basically do is While you are not near the destination, move to it, when finished pop the state.

Here is the StopLingering function :

/******************************************************************
 *
 *  TUTORIAL FUNCTION
 *
 *  This is a timer function, it prevents the MoveMouseClick state from
 *  looking to get stuck in an obstacle. After a set of seconds it
 *  pushes the entire state stack so the pawn revert to PlayerMove
 *  automatic state.
 *
 ******************************************************************/
function StopLingering()
{
	//Remove all current move state and query for input from now on.
	`Log("Stopped lingering...");
	PopState(true);
}

Do a compilation and if you did not miss a thing, everything should be fine. Run the game and click around, your pawn should move to a destination but a spring effect occurs. This is because the bPawnNearDestination is never set. We will fix this when a PlayerMove occurs. Now this function could be declared in each states, but again for simplicity we declare it at a global class level :

/******************************************************************
 *
 *  TUTORIAL FUNCTION
 *
 *  PlayerMove is called each frame, we declare it here inside the
 *  PlayerController so its general to all states. It can be possible
 *  to declare this function in each single state, having multiple
 *  PlayerMove scenario, but for the simplicity of the tutorial
 *  we have put it here in the class. It controls the player in that
 *  it does a distance check when moving. It calculates the remaining
 *  distance to the target. If target is within 2D(X,Y) offset, then
 *  set the var bPawnNearDestination for state control.
 *
 *  Rotation
 *
 *  This function overrides the controller rotation of the pawn. Depending
 *  on the situation (state) the pawn will either face a direction or rotate
 *  to face the destination.
 *
 ******************************************************************/
function PlayerMove(float DeltaTime)
{
	local Vector PawnXYLocation;
	local Vector DestinationXYLocation;
	local Vector    Destination;
	local Vector2D  DistanceCheck;          

	super.PlayerMove(DeltaTime);

	//Get player destination for a check on distance left. (calculate distance)
	Destination = GetDestinationPosition();
	DistanceCheck.X = Destination.X - Pawn.Location.X;
	DistanceCheck.Y = Destination.Y - Pawn.Location.Y;
	DistanceRemaining = Sqrt((DistanceCheck.X*DistanceCheck.X) + (DistanceCheck.Y*DistanceCheck.Y));

	//`Log("DistanceCheck is"@DistanceCheck.X@DistanceCheck.Y);
	//`Log("Distance remaining"@DistanceRemaining);

	bPawnNearDestination = DistanceRemaining < 15.0f;
	`Log("Has pawn come near destination ?"@bPawnNearDestination);

	PawnXYLocation.X = Pawn.Location.X;
	PawnXYLocation.Y = Pawn.Location.Y;

	DestinationXYLocation.X = GetDestinationPosition().X;
	DestinationXYLocation.Y = GetDestinationPosition().Y;

	Pawn.SetRotation(Rotator(DestinationXYLocation - PawnXYLocation));
}

Suffice to say that this function get the distance between each vector and store the result as DistanceRemaining (we only check X and Y no height checks here). If the distance is less than 15 units we consider that the pawn has attained destination. The rest of the function fixes the pawn rotation to face the destination. For the rotation to work perfectly, we need to override certain controller functions :

function UpdateRotation( float DeltaTime )
{
	/** This as been intentionnaly overriden and left blank for tutorial (see parent class to see what it does to controller) **/
}

function ProcessViewRotation( float DeltaTime, out Rotator out_ViewRotation, Rotator DeltaRot )
{
	/** This as been intentionnaly overriden and left blank for tutorial (see parent class to see what it does to controller) **/
}

Compile and test all this. Everything should work and when you click in the map, try to click beyond obstacle and see that the pawn does not react because he knows his path is blocked. This concludes chapter 3. Congratulations, you have a pawn that moves on mouse clicks. Next chapter will be about making the pawn go around when you press and hold the mouse button.

 

EDIT 22 FEB 2010 : Some of you might have experienced the pawn floating, here is the fix for that from the forum thread : In your pawn add this in default properties or whichever script file you define your character skeletalmesh

CollisionType=COLLIDE_BlockAll
Begin Object Name=CollisionCylinder
CollisionRadius=+0021.000000
CollisionHeight=+0048.000000
End Object
CylinderComponent=CollisionCylinder

 

Get chapter 3 source code

 

 

Chapter 4 - Making it follow the mouse

 

 

 

Update me when site is updated
Share and Enjoy:
  • Print this article!
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Slashdot
  • Turn this article into a PDF!
  • Twitter
  1. drzovil
    December 26th, 2009 at 17:49 | #1

    Thank you for your superb tutorial.
    It is exactly what I’ve wanted to see and expected to greatly expand my understanding on unreal script.

    If you don’t mind, I’d like to make a quick question about ‘State’.

    When looking through the logs, I found out that the ‘IsometircGamePlayerController’ starts in ‘PlayerWalking’ state.
    In PlayerController.uc, however, the initial state, that is having ‘auto’ keyword, is PlayerWaiting which extends BaseSpectating. What I’m curious about is how the ‘PlayerWaiting’ is changed into ‘PlayerWalking’ in your tutorial.
    Though I read through the related codes, the state changing point couldn’t be seen.

    Thank you in advance.

  2. Superman
    January 22nd, 2010 at 06:44 | #2

    @drzovil
    PlayerWaiting is skipped when bDelayedStart=false is set in the GameInfo class, as the PlayerWaiting state only exists during the spectator/initiation phase before the default countdown.

  3. Tony
    February 21st, 2010 at 17:29 | #3

    Cheers for this great tutorial, its working wonders so far.

    What i’m wondering if you know how to make it so instead of clicking to move the player, the player looks in the direction of the cursor and keeps WASD for movement. I’m working on a Slaughterhouse UT3 mod style game (If you’ve ever played it). Any help would be greatly appreciated.

    Thanks

  4. Hotdot
    February 21st, 2010 at 23:29 | #4

    Tony: In the default code, unless you redefine WSAD keys they do move as you asked, I think this is default playercontroller class binding. Check it up in the class itself in the engine folder. For the players head, there is a sort of look at that you can inspect. It is also located in playercontroller’s PlayerMove function if I recall. It is pretty obvious where they set the head’s direction. For complete rotations (not straffing) you would have to reimplement a custom playerMove and check for velocity added to the player and do some rotation. To achieve nice rotation over time you would have to use interpolation. Check in Object.uc, all vector and rotator interpolation functions are located there, using find in files with visual studio you can locate code snippet that does it.

  1. February 22nd, 2010 at 20:15 | #1
  2. March 28th, 2010 at 05:13 | #2