1. Code
  2. Coding Fundamentals
  3. Game Development

Create a Hockey Game AI Using Steering Behaviors: Game Mechanics

Scroll to top
This post is part of a series called Create AI for a Hockey Game Using Steering Behaviors.
Create a Hockey Game AI Using Steering Behaviors: Defense

In past posts in this series, we've focused on the concepts behind the artificial intelligence we've been learning about. In this part, we'll wrap all the implementation into an entirely playable hockey game. You'll learn how to add the missing pieces required to turn this into a game, such as score, power-ups, and a bit of game design.

Final Result

Below is the game that will be implemented using all the elements described in this tutorial.

Thinking Game Design

The previous parts of this series focused on explaining how the game AI works. Each part detailed a particular aspect of the game, like how athletes move and how attack and defense are implemented. They were based on concepts like steering behaviors and stack-based finite state machines.

In order to make a fully playable game, however, all those aspects must be wrapped into a core game mechanic. The most obvious choice would be to implement all the official rules of an official hockey match, but that would require a lot of work and time. Let's take a simpler fantasy approach instead.

All hockey rules will be replaced with a single one: if you are carrying the puck and are touched by an opponent, you freeze and shatter into a million pieces! It will make the game simpler to play and fun for both players: the one carrying the puck and the one trying to recover it.

In order to enhance this mechanic, we'll add a few power-ups. They will help the player to score and make the game a bit more dynamic.

Adding the Ability to Score

Let's begin with the scoring system, responsible for determining who wins or loses. A team scores every time the puck enters the opponent's goal.

The easiest way to implement this is by using two overlapped rectangles:

Overlapped rectangles describing the goal area If the puck collides with the red rectangle the team scoresOverlapped rectangles describing the goal area If the puck collides with the red rectangle the team scoresOverlapped rectangles describing the goal area If the puck collides with the red rectangle the team scores
Overlapped rectangles describing the goal area. If the puck collides with the red rectangle, the team scores.

The green rectangle represents the area occupied by the goal structure (the frame and the net). It works like a solid block, so the puck and the athletes will not be able to move through it; they will bounce back.

The red rectangle represents the "score area". If the puck overlaps this rectangle, it means a team just scored.

The red rectangle is smaller than the green one, and placed in front of it, so if the puck touches the goal on any side but the front, it will bounce back and no score will be added:

A few examples of how the puck would behave if it touched the rectangles while movingA few examples of how the puck would behave if it touched the rectangles while movingA few examples of how the puck would behave if it touched the rectangles while moving
A few examples of how the puck would behave if it touched the rectangles while moving.

Organizing Everything After Someone Scores

After a team scores, all athletes must return to their initial position and the puck must be placed at the rink center again. After this process, the match can continue.

Moving Athletes To Their Initial Position

As explained in the first part of this series, all athletes have an AI state called prepareForMatch that will move them towards the initial position, and cause them to smoothly come to a stop there.

When the puck overlaps one of the "score areas", any currently active AI state of all athlete is removed and prepareForMatch is pushed into the brain. Wherever the athletes are, they will return to their initial position after a few seconds:

Moving the Camera Towards the Rink Center

Since the camera always follows the puck, if it is directly teleported to the rink center after someone scores, the current view will abruptly change, which would be ugly and confusing.

A better way to do this is to move the puck smoothly towards the rink center; since the camera follows the puck, this will gracefully slide the view from the goal to the rink center. 

This can be achieved by changing the puck's velocity vector after it hits any goal area. The new velocity vector must "push" the puck towards the rink center, so it can be calculated as:

1
var c :Vector3D = getRinkCenter();
2
var p :Vector3D = puck.position;
3
4
var v :Vector3D = c - p;
5
v = normalize(v) * 100;
6
7
puck.velocity = v;

By subtracting the rink center's position from the puck's current position, it is possible to calculate a vector that points directly towards the rink center.

After normalizing this vector, it can be scaled by any value, like 100, which controls how fast the puck moves towards the rink center.

Below is an image with a representation of the new velocity vector:

Calculation of a new velocity vector that will move the puck towards the rink centerCalculation of a new velocity vector that will move the puck towards the rink centerCalculation of a new velocity vector that will move the puck towards the rink center
Calculation of a new velocity vector that will move the puck towards the rink center.

This vector V is used as the puck's velocity vector, so the puck will move towards the rink center as intended.

To prevent any weird behavior while the puck is moving towards the rink center, such as an interaction with an athlete, the puck is deactivated during the process. As a consequence, it stops interacting with athletes and is marked as invisible. The player will not see the puck moving, but the camera will still follow it.

In order to decide whether the puck is already in position, the distance between it and the rink center is calculated during the movement. If it is less than 10, for instance, the puck is close enough to be directly placed at the rink center and reactivated so that the match can continue.

Adding Power-Ups

The idea behind power-ups is to help the player achieve the game's primary objective, which is to score by carrying the puck to the opponent's goal.

For the sake of scope, our game will have only two power-ups: Ghost Help and Fear The Puck. The former adds three additional athletes to the player's team for some time, while the latter makes the opponents flee the puck for a few seconds.

Power-ups are added to both teams when anyone scores.

Implementing the "Ghost Help" Power-up

Since all athletes added by the Ghost Help power-up are temporary, the Athlete class must be modified to allow an athlete to be marked as a "ghost". If an athlete is a ghost, it will remove itself from the game after a few seconds.

Below is the Athlete class, highlighting only the additions made to accommodate the ghost functionality:

1
public class Athlete
2
{
3
    // (...)

4
	private var mGhost :Boolean;        // tells if the athlete is a ghost (a powerup that adds new athletes to help steal the puck).

5
	private var mGhostCounter :Number;  // counts the time a ghost will remain active

6
	
7
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
8
		// (...)

9
		mGhost = false;
10
		mGhostCounter = 0;
11
		
12
		// (...)

13
	}
14
    
15
    public function setGhost(theStatus :Boolean, theDuration :Number) :void {
16
		mGhost = theStatus;
17
		mGhostCounter = theDuration;
18
	}
19
	
20
	public function amIAGhost() :Boolean {
21
		return mGhost;
22
	}
23
	
24
	public function update() :void {
25
		// (...)

26
		
27
		// Update powerup counters and stuff

28
		updatePowerups();
29
		
30
		// (...)

31
	}
32
    
33
    public function updatePowerups() :void {
34
        // TODO.

35
    }
36
}

The property mGhost is a boolean that tells if the athlete is a ghost or not, while mGhostCounter contains the amount of seconds the athlete should wait before removing himself from the game.

Those two properties are used by the updatePowerups() method:

1
private function updatePowerups():void {		
2
	// If the athlete is a ghost, it has a counter that controls

3
	// when it must be removed.

4
	if (amIAGhost()) {
5
		mGhostCounter -= time_elapsed;
6
		
7
		if (mGhostCounter <= 2) {
8
			// Make athlete flicker when it is about to be removed.

9
			flicker(0.5);
10
		}
11
		
12
		if (mGhostCounter <= 0) {
13
			// Time to leave this world! (again)

14
			kill();
15
		}
16
	}
17
}

The updatePowerups() method, called within the athlete's update() routine, will handle all power-up processing in the athlete. Right now all it does is check whether the current athlete is a ghost or not. If it is, then the mGhostCounter property is decremented by the amount of time elapsed since the last update.

When the value of mGhostCounter reaches zero, it means that the temporary athlete has been active for long enough, so it must remove itself from the game. To make the player aware of that, the athlete will start flickering its last two seconds before disappearing.

Finally, it is time to implement the process of adding the temporary athletes when the power-up is activated. That is performed in the powerupGhostHelp() method, available in the main game logic:

1
private function powerupGhostHelp() :void {
2
	var aAthlete :Athlete;	
3
	
4
	for (var i:int = 0; i < 3; i++) {
5
        // Add the new athlete to the list of athletes

6
		aAthlete = addAthlete(RINK_WIDTH / 2, RINK_HEIGHT - 100);
7
        
8
        // Mark the athlete as a ghost which will be removed after 10 seconds.

9
		aAthlete.setGhost(true, 10);
10
	}
11
}

This method iterates over a loop that corresponds to the amount of temporary athletes being added. Each new athlete is added to the bottom of the rink and marked as a ghost. 

As previously described, ghost athletes will remove themselves from the game.

Implementing the "Fear The Puck" Power-Up

The Fear The Puck power-up makes all opponents flee the puck for a few seconds. 

Just like the Ghost Help power-up, the Athlete class must be modified to accommodate that functionality:

1
public class Athlete
2
{
3
    // (...)

4
    private var mFearCounter :Number; // counts the time the athlete should evade from puck (when fear powerup is active).

5
6
	
7
	public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
8
		// (...)

9
		mFearCounter = 0;
10
		
11
		// (...)

12
	}
13
    
14
	public function fearPuck(theDuration: Number = 2) :void {
15
		mFearCounter = theDuration;
16
	}
17
	
18
    // Returns true if the mFearCounter has a value and the athlete

19
    // is not idle or preparing for a match.

20
	private function shouldIEvadeFromPuck() :Boolean {
21
		return mFearCounter > 0 && mBrain.getCurrentState() != idle && mBrain.getCurrentState() != prepareForMatch;
22
	}
23
    
24
    private function updatePowerups():void {
25
    	if(mFearCounter > 0) {
26
			mFearCounter -= elapsed_time;
27
		}
28
		
29
		// (...)

30
	}
31
	
32
	public function update() :void {
33
		// (...)

34
		
35
		// Update powerup counters and stuff

36
		updatePowerups();
37
        
38
		// If the athlete is an AI-controlled opponent

39
		if (amIAnAiControlledOpponent()) {
40
			// Check if "fear of the puck" power-up is active.

41
            // If that's true, evade from puck.

42
			if(shouldIEvadeFromPuck()) {
43
				evadeFromPuck();
44
			}
45
        }			
46
		
47
		// (...)

48
	}
49
    
50
    public function evadeFromPuck() :void {
51
        // TODO

52
    }
53
}

First the updatePowerups() method is changed to decrement the mFearCounter property, which contains the amount of time the athlete should avoid the puck. The mFearCounter property is changed every time the method fearPuck() is called.

In the Athlete's update() method, a test is added to check if the power-up should take place. If the athlete is an opponent controlled by the AI (amIAnAiControlledOpponent() returns true) and the athlete should evade the puck (shouldIEvadeFromPuck() returns true as well), the evadeFromPuck() method is invoked.

The evadeFromPuck() method uses the evade behavior, which makes an entity avoid any object and its trajectory altogether:

1
private function evadeFromPuck() :void {
2
	mBoid.steering = mBoid.steering + mBoid.evade(getPuck().getBoid());
3
}

All the evadeFromPuck() method does is to add an evade force to the current athlete's steering force. It makes him evade the puck without ignoring the already added steering forces, such as the one created by the currently active AI state.

In order to be evadable, the puck must behave like a boid, as all athletes do (more information about that in the first part of the series). As a consequence, a boid property, which contains the puck's current position and velocity, must be added to the Puck class:

1
class Puck {
2
    // (...)

3
    private var mBoid :Boid;
4
    
5
    // (...)

6
    
7
    public function update() {
8
        // (...)

9
        mBoid.update();
10
    }
11
    
12
    public function getBoid() :Boid {
13
        return mBoid;
14
    }
15
    
16
    // (...)

17
}

Finally, we update the main game logic to make opponents fear the puck when the power-up is activated:

1
private function powerupFearPuck() :void {
2
	var i           :uint,
3
		athletes    :Array  = rightTeam.members,
4
		size        :uint   = athletes.length;
5
			
6
	for (i = 0; i < size; i++) {
7
		if (athletes[i] != null) {
8
            // Make athlete fear the puck for 3 seconds.

9
			athletes[i].fearPuck(3);
10
		}
11
	}
12
}

The method iterates over all opponent athletes (the right team, in this case), calling the fearkPuck() method of each one of them. This will trigger the logic that makes the athletes fear the puck during a few seconds, as previously explained.

Freezing and Shattering

The last addition to the game is the freezing and shattering part. It is performed in the main game logic, where a routine checks whether the athletes of the left team are overlapping with the athletes of the right team.

This overlapping check is automatically performed by the Flixel game engine, which invokes a callback every time an overlap is found:

1
private function athletesOverlapped(theLeftAthlete :Athlete, theRightAthlete :Athlete) :void {
2
    // Does the puck have an owner?	

3
	if (mPuck.owner != null) {
4
        // Yes, it does.

5
		if (mPuck.owner == theLeftAthlete) {
6
            //Puck's owner is the left athlete

7
			theLeftAthlete.shatter();
8
			mPuck.setOwner(theRightAthlete);
9
10
		} else if (mPuck.owner == theRightAthlete) {
11
            //Puck's owner is the right athlete

12
			theRightAthlete.shatter();
13
			mPuck.setOwner(theLeftAthlete);
14
		}
15
	}
16
}

This callback receives as parameters the athletes of each team that overlapped. A test checks if the puck's owner is not null, which means it is being carried by someone.

In that case, the puck's owner is compared to the athletes that just overlapped. If one of them is carrying the puck (so he is the puck's owner), he is shattered and the puck's ownership passes to the other athlete.

The shatter() method in the Athlete class will mark the athlete as inactive and place it at the bottom of the rink after a few seconds. It will also emit several particles representing ice pieces, but this topic will be covered in another post.

Conclusion

In this tutorial, we implemented a few elements required to turn our hockey prototype into a fully playable game. I intentionally placed the focus on the concepts behind each of those elements, instead of how to actually implement them in game engine X or Y.

The freeze and shatter approach used for the game might sound too fantastical, but it helps keep the project manageable. Sports rules are very specific, and their implementation can be tricky.

By adding a few screens and some HUD elements, you can create your own full hockey game from this demo!

References

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.