How to make a Pong game in Haxe and OpenFL: Part 5

posted on Sep 12, 2014 in Haxe, OpenFL, Game design
Haxe OpenFL Pong game

It's time to finish up our game. This time we're adding ball bounce behavior and AI movement.

As a gameplay improvement we'll create a bit more complex expression for randomizing the initial angle of the ball movement. The reason is that sometimes the ball would go at a very high angle, bouncing up and down for a long time before reaching any of the edges.

Let's break the expression into two parts - first let's randomize the direction (left or right), then randomize an angle in a 90 degree range, facing left or right.

Go to your setGameState() method and update the ball movement angle code:

var direction:Int = (Math.random() > .5)?(1):( -1);
var randomAngle:Float = (Math.random() * Math.PI / 2) - 45;
ballMovement.x = direction * Math.cos(randomAngle) * ballSpeed;
ballMovement.y = Math.sin(randomAngle) * ballSpeed;

The first line determines the direction multiplier - either 1 or -1. This is done by first creating a random value from 0 to 1 using Math.random(), then checking whether the value is greater than 0.5. If it is, set the multiplier to 1, otherwise -1.

The if statement is written in shortened form, the syntax is:

(condition)?(true_value):(false_value);

Based on the same logic, let's create a bounceBall() method, which will randomize the ball's angle using the same logic. The only difference is that the direction of the ball will be opposite of its current direction. The current direction can be determined easily: if the current ball movement on the x axis is positive, that means the ball is moving right. Otherwise, the ball is moving left.

private function bounceBall():Void {
	var direction:Int = (ballMovement.x > 0)?( -1):(1);
	var randomAngle:Float = (Math.random() * Math.PI / 2) - 45;
	ballMovement.x = direction * Math.cos(randomAngle) * ballSpeed;
	ballMovement.y = Math.sin(randomAngle) * ballSpeed;
}

This function will be called when the ball needs to bounce off a platform.

Let's add a mechanism that checks when the ball needs to bounce. Create a new if statement inside of your ENTER_FRAME handler:

if (ballMovement.x < 0 && ball.x < 30 && ball.y >= platform1.y && ball.y <= platform1.y + 100) {
	bounceBall();
	ball.x = 30;
}

We first check whether the ball's movement direction is left. Secondly, check if the ball's x coordinate is below 30 - right about where it contacts the platform. Then check if the ball's y coordinate is between the platform's top point and bottom point.

When all of those conditions are true, bounce the ball and set its position to 30 *(the edge of the platform).

Same logic can be applied when adding bounce functionality to the AI's platform, just change the coordinates:

if (ballMovement.x > 0 && ball.x > 470 && ball.y >= platform2.y && ball.y <= platform2.y + 100) {
	bounceBall();
	ball.x = 470;
}

Now let's add AI platform movement.

The logic here is simple - wait until the ball is about 2/5 of the screen's width away from the enemy platform, then move the platform vertically in the direction of the ball. The first condition is there to add fairness, or else the AI will never lose a game. This 2/5 distance will simulate AI's reaction time.

Here's the code to add to the ENTER_FRAME handler:

// AI platform movement
if (ball.x > 300 && ball.y > platform2.y + 70) {
	platform2.y += platformSpeed;
}
if (ball.x > 300 && ball.y < platform2.y + 30) {
	platform2.y -= platformSpeed;
}

You can see that I make the AI try to hit the ball with the middle part of his platform, again, to simulate human play.

Finally, add limits on how far can AI's platform go:

// AI platform limits
if (platform2.y < 5) platform2.y = 5;
if (platform2.y > 395) platform2.y = 395;

Test your game now. You should now be able to play pong against an AI opponent, who is strong, but beatable!

Haxe OpenFL Pong game

And here's the full Main.hx code:

package ;

import flash.display.Sprite;
import flash.events.Event;
import flash.Lib;
import openfl.events.KeyboardEvent;
import openfl.geom.Point;
import openfl.text.TextField;
import openfl.text.TextFormat;
import openfl.text.TextFormatAlign;

enum GameState {
	Paused;
	Playing;
}

enum Player {
	Human;
	AI;
}

class Main extends Sprite 
{
	var inited:Bool;
	
	private var platform1:Platform;
	private var platform2:Platform;
	private var ball:Ball;
	private var scorePlayer:Int;
	private var scoreAI:Int;
	private var scoreField:TextField;
	private var messageField:TextField;
	private var currentGameState:GameState;
	private var arrowKeyUp:Bool;
	private var arrowKeyDown:Bool;
	private var platformSpeed:Int;
	private var ballMovement:Point;
	private var ballSpeed:Int;

	/* ENTRY POINT */
	
	function resize(e) 
	{
		if (!inited) init();
		// else (resize or orientation change)
	}
	
	function init() 
	{
		if (inited) return;
		inited = true;
		
		platform1 = new Platform();
		platform1.x = 5;
		platform1.y = 200;
		this.addChild(platform1);
		
		platform2 = new Platform();
		platform2.x = 480;
		platform2.y = 200;
		this.addChild(platform2);
		
		ball = new Ball();
		ball.x = 250;
		ball.y = 250;
		this.addChild(ball);
		
		var scoreFormat:TextFormat = new TextFormat("Verdana", 24, 0xbbbbbb, true);
		scoreFormat.align = TextFormatAlign.CENTER;
		
		scoreField = new TextField();
		addChild(scoreField);
		scoreField.width = 500;
		scoreField.y = 30;
		scoreField.defaultTextFormat = scoreFormat;
		scoreField.selectable = false;
		
		var messageFormat:TextFormat = new TextFormat("Verdana", 18, 0xbbbbbb, true);
		messageFormat.align = TextFormatAlign.CENTER;
		
		messageField = new TextField();
		addChild(messageField);
		messageField.width = 500;
		messageField.y = 450;
		messageField.defaultTextFormat = messageFormat;
		messageField.selectable = false;
		messageField.text = "Press SPACE to start\nUse ARROW KEYS to move your platform";
		
		scorePlayer = 0;
		scoreAI = 0;
		arrowKeyUp = false;
		arrowKeyDown = false;
		platformSpeed = 7;
		ballSpeed = 7;
		ballMovement = new Point(0, 0);
		setGameState(Paused);
		
		stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown);
		stage.addEventListener(KeyboardEvent.KEY_UP, keyUp);
		this.addEventListener(Event.ENTER_FRAME, everyFrame);
	}
	
	private function setGameState(state:GameState):Void {
		currentGameState = state;
		updateScore();
		if (state == Paused) {
			messageField.alpha = 1;
		}else {
			messageField.alpha = 0;
			platform1.y = 200;
			platform2.y = 200;
			ball.x = 250;
			ball.y = 250;
			var direction:Int = (Math.random() > .5)?(1):( -1);
			var randomAngle:Float = (Math.random() * Math.PI / 2) - 45;
			ballMovement.x = direction * Math.cos(randomAngle) * ballSpeed;
			ballMovement.y = Math.sin(randomAngle) * ballSpeed;
		}
	}
	
	private function keyDown(event:KeyboardEvent):Void {
		if (currentGameState == Paused && event.keyCode == 32) { // Space
			setGameState(Playing);
		}else if (event.keyCode == 38) { // Up
			arrowKeyUp = true;
		}else if (event.keyCode == 40) { // Down
			arrowKeyDown = true;
		}
	}
	
	private function keyUp(event:KeyboardEvent):Void {
		if (event.keyCode == 38) { // Up
			arrowKeyUp = false;
		}else if (event.keyCode == 40) { // Down
			arrowKeyDown = false;
		}
	}
	
	private function everyFrame(event:Event):Void {
		if (currentGameState == Playing) {
			// player platform movement
			if (arrowKeyUp) {
				platform1.y -= platformSpeed;
			}
			if (arrowKeyDown) {
				platform1.y += platformSpeed;
			}
			// AI platform movement
			if (ball.x > 300 && ball.y > platform2.y + 70) {
				platform2.y += platformSpeed;
			}
			if (ball.x > 300 && ball.y < platform2.y + 30) {
				platform2.y -= platformSpeed;
			}
			// player platform limits
			if (platform1.y < 5) platform1.y = 5;
			if (platform1.y > 395) platform1.y = 395;
			// AI platform limits
			if (platform2.y < 5) platform2.y = 5;
			if (platform2.y > 395) platform2.y = 395;
			// ball movement
			ball.x += ballMovement.x;
			ball.y += ballMovement.y;
			// ball platform bounce
			if (ballMovement.x < 0 && ball.x < 30 && ball.y >= platform1.y && ball.y <= platform1.y + 100) {
				bounceBall();
				ball.x = 30;
			}
			if (ballMovement.x > 0 && ball.x > 470 && ball.y >= platform2.y && ball.y <= platform2.y + 100) {
				bounceBall();
				ball.x = 470;
			}
			// ball edge bounce
			if (ball.y < 5 || ball.y > 495) ballMovement.y *= -1;
			// ball goal
			if (ball.x < 5) winGame(AI);
			if (ball.x > 495) winGame(Human);
		}
	}
	
	private function bounceBall():Void {
		var direction:Int = (ballMovement.x > 0)?( -1):(1);
		var randomAngle:Float = (Math.random() * Math.PI / 2) - 45;
		ballMovement.x = direction * Math.cos(randomAngle) * ballSpeed;
		ballMovement.y = Math.sin(randomAngle) * ballSpeed;
	}
	
	private function winGame(player:Player):Void {
		if (player == Human) {
			scorePlayer++;
		} else {
			scoreAI++;
		}
		setGameState(Paused);
	}
	
	private function updateScore():Void {
		scoreField.text = scorePlayer + ":" + scoreAI;
	}

	/* SETUP */

	public function new() 
	{
		super();	
		addEventListener(Event.ADDED_TO_STAGE, added);
	}

	function added(e) 
	{
		removeEventListener(Event.ADDED_TO_STAGE, added);
		stage.addEventListener(Event.RESIZE, resize);
		#if ios
		haxe.Timer.delay(init, 100); // iOS 6
		#else
		init();
		#end
	}
	
	public static function main() 
	{
		// static entry point
		Lib.current.stage.align = flash.display.StageAlign.TOP_LEFT;
		Lib.current.stage.scaleMode = flash.display.StageScaleMode.NO_SCALE;
		Lib.current.addChild(new Main());
		//
	}
}

We've now built a cross-platform Pong game using Haxe and OpenFL! The principles used in this tutorial series can be also used when developing larger scale games.

Take a look at my other tutorials if you'd like to learn more about developing games and not only games in Haxe.

12548