Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Create a Hockey Game Using Steering Behaviors: Foundation

$
0
0

There are different ways to make any particular game. Usually, a developer chooses something that fits his skills, using the techniques he already knows to produce the best result possible. Sometimes, people don't yet know that they need a certain technique—perhaps even an easier and better one—simply because they already know a way to create that game. 

In this series of tutorials, you will learn how to create a hockey game using a combination of techniques, such as steering behaviors, that I've previously explained as concepts.

Note: Although this tutorial is written using AS3 and Flash, you should be able to use the same techniques and concepts in almost any game development environment.


Introduction

Hockey is a fun and popular sport and, as a video game, it incorporates many gamedev topics, such as movement patterns, teamwork (attack, defense), artificial intelligence, and tactics. A playable hockey game is a great fit to demonstrate the combination of some useful techniques.

To simulate the hockey mechanic, with athletes running and moving around, is a challenge. If the movement patterns are pre-defined, even with different paths, the game becomes predictable (and boring). How can we implement such a dynamic environment while still maintaining control over what is going on? The answer is: using steering behaviors.

Steering behaviors aim to create realistic movement patterns with improvisational navigation. They are based on simple forces that are combined every game update, so they are extremely dynamic by nature. This makes them the perfect choice for implementing something as complex and dynamic as a hockey or a soccer game.

Scoping the Work

For the sake of time and teaching, let's reduce the scope of the game a bit. Our hockey game will follow just a small set of the sport's original rules: in our game there will be no penalties and no goal keepers, so every athlete can move around the rink:

Hockey game using simplified rules.

Each goal will be replaced by a small "wall" with no net. In order to score, a team must move the puck (the disk) to make it touch any side of the opponent's goal. When someone scores, both teams will re-organize, and the puck will be placed at the center; the match will restart a few seconds after that.

Regarding the puck handling: if an athlete, say A, has the puck, and is touched by an opponent, say B, then B gains the puck and A becomes immovable for a few seconds. If the puck ever leaves the rink, it will be placed at the rink center immediately.

I will use the Flixel game engine to take care of the graphical part of the code. However, the engine code will be simplified or omitted in the examples, to keep the focus on the game itself.

Structuring the Environment

Let's begin with the game environment, which is composed of a rink, a number of athletes, and two goals. The rink is made of four rectangles placed around the ice area; these rectangles will collide with everything that touches them, so nothing will leave the ice area.

An athlete will be described by the Athlete class:

public class Athlete
{	
	private var mBoid :Boid; // controls the steering behavior stuff
	private var mId   :int;  // a unique identifier for the athelete
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
		mBoid = new Boid(thePosX, thePosY, theTotalMass);
	}
	public function update():void {
		// Clear all steering forces
		mBoid.steering = null;
		// Wander around
		wanderInTheRink();

		// Update all steering stuff
		mBoid.update();
	}
	
	private function wanderInTheRink() :void {
		var aRinkCenter :Vector3D = getRinkCenter();
		
		// If the distance from the center is greater than 80,
		// move back to the center, otherwise keep wandering.
		if (Utils.distance(this, aRinkCenter) >= 80) {
			mBoid.steering = mBoid.steering + mBoid.seek(aRinkCenter);
		} else {
			mBoid.steering = mBoid.steering + mBoid.wander();
		}
	}
}

The property mBoid is an instance of the Boid class, an encapsulation of the math logic used in the steering behaviors series. The mBoid instance has, among other elements, math vectors describing the current direction, steering force, and position of the entity.

The update() method in the Athlete class will be invoked every time the game updates. For now, it only clears any active steering force, adds a wander force, and finally calls mBoid.update(). The former command updates all the steering behavior logic encapsulated within mBoid, making the athlete move (using Euler integration).

The game class, which is responsible for the game loop, will be called PlayState. It has the rink, two groups of athletes (one group for each team) and two goals:

public class PlayState
{
    private var mAthletes  :FlxGroup;
	private var mRightGoal :Goal;
	private var mLeftGoal  :Goal;

	public function create():void {
		// Here everything is created and added to the screen.
	}
		
	override public function update():void {
		// Make the rink collide with athletes
		collide(mRink, mAthletes);
		
		// Ensure all athletes will remain inside the rink.
		applyRinkContraints();
	}
    
    private function applyRinkContraints() :void {
        // check if athletes are within the rink
        // boundaries.
    }
}

Assuming that a single athlete was added to the match, below is the result of everything so far:

Following the Mouse Cursor

The athlete must follow the mouse cursor, so the player can actually control something. Since the mouse cursor has a position on the screen, it can be used as the destination for the arrival behavior.

The arrival behavior will make an athlete seek the cursor position, smoothly slow down the velocity as it approaches the cursor, and eventually stop there. 

In the Athlete class, let's replace the wandering method with the arrival behavior:

public class Athlete
{    
	// (...)
	public function update():void {
		// Clear all steering forces
		mBoid.steering = null;
    	// The athlete is controlled by the player,
        // so just follow the mouse cursor.
		followMouseCursor();

		// Update all steering stuff
		mBoid.update();
	}
	
    private function followMouseCursor() :void {
		var aMouse :Vector3D = getMouseCursorPosition();
		mBoid.steering = mBoid.steering + mBoid.arrive(aMouse, 50);
	}
}

The result is an athlete that can the mouse cursor. Since the movement logic is based on steering behaviors, the athletes navigate the rink in a convincing and smooth way. 

Use the mouse cursor to guide the athlete in the demo below:

Adding and Controlling the Puck

The puck will be represented by the class Puck. The most important parts are the update() method and the mOwner property:

public class Puck
{
    public var velocity :Vector3D;
    public var position :Vector3D;
    private var mOwner :Athlete;	// the athlete currently carrying the puck.
	public function setOwner(theOwner :Athlete) :void {
		if (mOwner != theOwner) {
			mOwner = theOwner;
			velocity = null;
		}
	}
	public function update():void {
	}
	public function get owner() :Athlete { return mOwner; }
}

Following the same logic of the athlete, the puck's update() method will be invoked every time the game updates. The mOwner property determines whether the puck is in possession of any athlete. If mOwner is null, it means the puck is "free", and it will move around, eventually bouncing off the rink walks.

If mOwner is not null, it means that the puck is being carried by an athlete. In this case, it will ignore any collision checks and will be forcefully placed ahead of the athlete. This can be achieved using the athlete's velocity vector, which also matches the athlete's direction:

Explanation of how the puck is placed ahead of the athlete.

The ahead vector is a copy of the athlete's velocity vector, so they point in the same direction. After ahead is normalized, it can be scaled by any value—say, 30—to control how far the puck will be placed ahead of the athlete.

Finally, the puck's position receives the athlete's position added to ahead, placing the puck at the desired position. 

Below is the code for all that:

public class Puck
{
	// (...)
    private function placeAheadOfOwner() :void {
		var ahead :Vector3D = mOwner.boid.velocity.clone();
		ahead = normalize(ahead) * 30;
		position = mOwner.boid.position + ahead;
	}
	override public function update():void {
		if (mOwner != null) {
			placeAheadOfOwner();
		}
	}
    // (...)
}

In the PlayState class, there is a collision test to check whether the puck overlaps any athlete. If it does, the athlete that just touched the puck becomes its new owner. The result is a puck that "sticks" to the athlete. In the below demo, guide the athlete to touch the puck at the center of the rink to see this in action:


Hitting the Puck

It's time to make the puck move as a result of being hit by the stick. Regardless of the athlete carrying the puck, all that is required to simulate a hit by the stick is to calculate a new velocity vector. That new velocity will move the puck towards the desired destination.

A velocity vector can be generated by one position vector from another; the newly generated vector will then go from one position to another. That's exactly what is needed to calculate the puck's new velocity vector after a hit:

Calculation of puck's new velocity after a hit from the stick.

In the image above, the destination point is the mouse cursor. The puck's current position can be used as the starting point, while the point where the puck should be after it has been hit by the stick can be used as the ending point. 

The pseudo-code below shows the implementation of goFromStickHit(), a method in the Puck class that implements the logic illustrated in the image above:

public class Puck
{
    // (...)

    public function goFromStickHit(theAthlete :Athlete, theDestination :Vector3D, theSpeed :Number = 160) :void {
		// Place the puck ahead of the owner to prevent unexpected trajectories
        // (e.g. puck colliding the athlete that just hit it)
		placeAheadOfOwner();
		
		// Mark the puck as free (no owner)
		setOwner(null);
		
		// Calculate the puck's new velocity
        var new_velocity :Vector3D = theDestination - position;
		velocity = normalize(new_velocity) * theSpeed;
	}
}

The new_velocity vector goes from the puck's current position to the target (theDestination). After that, it is normalized and scaled by theSpeed, which defines the magnitude (length) of new_velocity. That operation, in other words, defines how fast the puck will move from its current position to the destination. Finally, the puck's velocity vector is replaced by new_velocity.

In the PlayState class, the goFromStichHit() method is invoked every time the player clicks the screen. When it happens, the mouse cursor is used as the destination for the hit. The result is seen in this demo:

Adding the A.I.

So far, we've had just a single athlete moving around the rink. As more athletes are added, the AI must be implemented to make all these athletes look like they are alive and thinking.

In order to achieve that, we'll use a stack-based finite state machine (stack-based FSM, for short). As previously described, FSMs are versatile and useful for implementing AI in games. 

For our hockey game, a property named mBrain will be added to the Athlete class:

public class Athlete
{    
    // (...)
    private var mBrain :StackFSM; // controls the AI stuff
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
		// (...)
        mBrain = new StackFSM();
	}
    // (...)
}

This property is an instance of StackFSM, a class previously used in the FSM tutorial. It uses a stack to control the AI states of an entity. Every state is described as a method; when a state is pushed into the stack, it becomes the active method and is called during every game update.

Each state will perform a specific task, such as moving the athlete towards the puck. Every state is responsible for ending itself, which means it is responsible for popping itself from the stack.

The athlete can be controlled by the player or by the AI now, so the update() method in the Athlete class must be modified to check that situation:

public class Athlete
{    
    // (...)
    public function update():void {
		// Clear all steering forces
		mBoid.steering = null;
        if (mControlledByAI) {
			// The athlete is controlled by the AI. Update the brain (FSM) and
			// stay away from rink walls.
			mBrain.update();
		} else {
			// The athlete is controlled by the player, so just follow
			// the mouse cursor.
			followMouseCursor();
		}
		// Update all steering stuff
		mBoid.update();
	}
}

If the AI is active, mBrain is updated, which invokes the currently active state method, making the athlete behave accordingly. If the player is in control, mBrain is ignored all together and the athlete moves as guided by the player. 

Regarding the states to push into the brain: for now let's implement just two of them. One state will let an athlete prepare himself for a match; when preparing for the match, an athlete will move to his position in the rink and stand still, staring at the puck. The other state will make the athlete simply stand still and stare at the puck.

In the next sections, we'll implement these states.

The Idle State

If the athlete is in the idle state, he will stop moving and stare at the puck. This state is used when the athlete is already in position in the rink and is waiting for something to happen, like the start of the match.

The state will be coded in the Athlete class, under the idle() method:

public class Athlete
{    
    // (...)
    public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) {
		// (...)
		// Tell the brain the current state is 'idle'
		mBrain.pushState(idle);
	}
    private function idle() :void {
		var aPuck :Puck = getPuck();
		stopAndlookAt(aPuck.position);
	}
    private function stopAndlookAt(thePoint :Vector3D) :void {
		mBoid.velocity = thePoint - mBoid.position;
		mBoid.velocity = normalize(mBoid.velocity) * 0.01;
	}
}

Since this method doesn't pop itself from the stack, it will remain active forever. In the future, this state will pop itself to make room for other states, such as attack, but for now it does the trick.

The stopAndStareAt() method follows the same principle used to calculate the puck's velocity after a hit.  A vector from the athlete's position to the puck's position is calculated by thePoint - mBoid.position and used as the athlete's new velocity vector.

That new velocity vector will move the athlete towards the puck. To ensure that the athlete will not move, the vector is scaled by 0.01 , "shrinking" its length to almost zero. It makes the athlete stop moving, but keeps him staring at the puck.

Preparing For a Match

If the athlete is in the prepareForMatch state, he will move towards his initial position, smoothly stopping there. The initial position is where the athlete should be right before the match starts. Since the athlete should stop at the destination, the arrival behavior can be used again:

public class Athlete
{    
    // (...)
    private var mInitialPosition :Vector3D;	// the position in the rink where the athlete should be placed
    public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number, theTeam :FlxGroup) {
        // (...)
        mInitialPosition = new Vector3D(thePosX, thePosY);
		// Tell the brain the current state is 'idle'
		mBrain.pushState(idle);
	}
    private function prepareForMatch() :void {
		mBoid.steering = mBoid.steering + mBoid.arrive(mInitialPosition, 80);
		// Am I at the initial position?
		if (distance(mBoid.position, mInitialPosition) <= 5) {
            // I'm in position, time to stare at the puck.
			mBrain.popState();
			mBrain.pushState(idle);
		}
	}
    // (...)
}

The state uses the arrival behavior to move the athlete towards the initial position. If the distance between the athlete and his initial position is less than 5, it means the athlete has arrived at the desired place. When this happens, prepareForMatch pops itself from the stack and pushes idle, making it the new active state.

Below is the result of using a stack-based FSM to control several athletes. Press G to place them at random positions in the rink, pushing the prepareForMatch state:


Conclusion

This tutorial presented the foundations to implement a hockey game using steering behaviors and stack-based finite state machines. Using a combination of those concepts, an athlete is able to move in the rink, following the mouse cursor. The athlete can also hit the puck towards a destination.

Using two states and a stack-based FSM, the athletes can re-organize and move to their position in the rink, preparing for the match.

In the next tutorial, you will learn how to make the athletes attack, carrying the puck towards the goal while avoiding opponents.

References


Viewing all articles
Browse latest Browse all 728

Trending Articles