PongPartOne

From C4 Engine Wiki

Jump to: navigation, search

Status: C4 2.0


Contents

Summary

Part one of this series presents a complete pong game where the paddles are controlled by a simple AI.

Features

  • Loads a world in code
  • Iterates through nodes in a world
  • Creates two different controllers and attaches them to world nodes
  • Prints the score to the command console
  • Exits the application at the press of the escape key
  • Converts integers to a string
  • Retrieves and compares the name of a node in the world


Header File: PongPt01.h

#ifndef PongPt01_h
#define PongPt01_h
 
#include "C4Application.h"
#include "C4Engine.h"
#include "C4Input.h"
#include "C4World.h"
#include "C4Cameras.h"
 
 
extern "C"
{
	C4MODULEEXPORT C4::Application *ConstructApplication(void);
}
 
namespace C4
{
 
	enum
	{
		kControllerBall = 'ball',
		kControllerPaddle = 'pddl'
	};
 
	class BallController : public RigidBodyController
	{
		public:
 
			BallController();
			~BallController();
 
			static bool ValidNode(const Node *node);
 
			void Preprocess(void);
 
			void Move(void);
	};
 
	class PaddleController : public Controller
	{
		private:
 
			static float paddleSpeed;
			long playerNumber;
 
		public:
			PaddleController(long player);
			~PaddleController();
 
			void Preprocess(void);
 
			void Move(void);
	};
 
	class GameWorld : public World
	{
		private:
 
			SpectatorCamera spectatorCamera;
 
 
		public:
 
			GameWorld(const char *name);
			~GameWorld();
 
			WorldResult Preprocess(void);
			void Render(void);
	};
 
	class Game : public Singleton<Game>, public Application
	{
		private:
 
			DisplayEventHandler displayEventHandler;
 
			InputMgr::KeyProc *prevEscapeProc;
			void *prevEscapeData;
 
			static World *ConstructWorld(const char *name, void *data);
			static void HandleDisplayEvent(EventType eventType, long param, void *data);
			static void EscapeProc(void *data);
 
			BallController *ballController;
 
			PaddleController *paddleController1;
			PaddleController *paddleController2;
 
			long playerOneScore;
			long playerTwoScore;
 
 
		public:
 
			Game();
			~Game();
 
			BallController *GetBallController();
			PaddleController *GetFirstPaddleController();
			PaddleController *GetSecondPaddleController();
 
			void IncreaseScore(bool player);
	};
 
	extern Game *TheGame;
}
 
#endif


Source File: PongPt01.cpp

#include "PongPt01.h"
 
using namespace C4;
 
 
Game *C4::TheGame = nullptr;
 
C4::Application *ConstructApplication(void)
{
	return (new Game);
}
 
 
BallController::BallController() : RigidBodyController(kControllerBall)
{
}
 
BallController::~BallController()
{
}
 
bool BallController::ValidNode(const Node *node)
{
	return (node->GetNodeType() == kNodeModel);
}
 
void BallController::Preprocess(void)
{
	RigidBodyController::Preprocess();
 
	SetRestitutionCoefficient(1.0F); 
	SetGravityMultiplier(0.0F);
 
	SetLinearVelocity(Vector3D(Math::RandomFloat(6.0F)-3.0F, 0.0F, Math::RandomFloat(2.0F)-1.0F).Normalize() * 50.0F);
}
 
void BallController::Move()
{
	Node *targetNode=GetTargetNode();
	if (targetNode->GetNodePosition().x > 25)
	{	
		SetRigidBodyPosition(targetNode->GetSuperNode()->GetInverseWorldTransform() * Point3D(0,0,15));
		SetLinearVelocity(Vector3D(Math::RandomFloat(6.0F)-3.0F, 0.0F, Math::RandomFloat(2.0F)-1.0F).Normalize() * 50.0F);
		TheGame->IncreaseScore(true);
	}
	else if (targetNode->GetNodePosition().x < -25)
	{
		SetRigidBodyPosition(targetNode->GetSuperNode()->GetInverseWorldTransform() * Point3D(0,0,15));
		SetLinearVelocity(Vector3D(Math::RandomFloat(6.0F)-3.0F, 0.0F, Math::RandomFloat(2.0F)-1.0F).Normalize() * 50.0F);
		TheGame->IncreaseScore(false);
	}
}
 
 
float PaddleController::paddleSpeed=0.025F;
 
PaddleController::PaddleController(long player) : Controller(kControllerPaddle)
{
	playerNumber=player;
}
 
PaddleController::~PaddleController()
{
}
 
void PaddleController::Preprocess(void)
{
	Controller::Preprocess();
}
 
void PaddleController::Move(void)
{
	Node *node=GetTargetNode();
	float ballZ=TheGame->GetBallController()->GetTargetNode()->GetWorldPosition().z;
	float ballDirection=TheGame->GetBallController()->GetLinearVelocity().x;
	Point3D paddlePosition=node->GetSuperNode()->GetWorldTransform() * node->GetNodePosition();
 
	if ((ballDirection<0.0F && playerNumber==1) || (ballDirection>0.0F && playerNumber==2))
	{
 
		if (ballZ>paddlePosition.z+3)
		{
			node->SetNodePosition(node->GetSuperNode()->GetInverseWorldTransform() * Point3D(paddlePosition.x,paddlePosition.y,Fmin(paddlePosition.z+Fmin(paddleSpeed,ballZ-paddlePosition.z),25.0F)));
			node->Invalidate();
		}
		else if (ballZ<paddlePosition.z+3)
		{
			node->SetNodePosition(node->GetSuperNode()->GetInverseWorldTransform() * Point3D(paddlePosition.x,paddlePosition.y,Fmax(paddlePosition.z-Fmin(paddleSpeed,paddlePosition.z-ballZ+3),1.0F)));
			node->Invalidate();
		}
	}
	else
	{
		if (paddlePosition.z>13)
		{
 
			node->SetNodePosition(node->GetSuperNode()->GetInverseWorldTransform() * Point3D(paddlePosition.x,paddlePosition.y,Fmin(paddlePosition.z-Fmin(paddleSpeed,paddlePosition.z+3-16),25.0F)));
			node->Invalidate();
		}
		else if (paddlePosition.z<13)
		{
 
			node->SetNodePosition(node->GetSuperNode()->GetInverseWorldTransform() * Point3D(paddlePosition.x,paddlePosition.y,Fmax(paddlePosition.z+Fmin(paddleSpeed,16-paddlePosition.z+3),1.0F)));
			node->Invalidate();
		}
	}
}
 
 
GameWorld::GameWorld(const char *name) : World(name),spectatorCamera(2.0F, 1.0F, 0.0F)
{
}
 
GameWorld::~GameWorld()
{
}
 
WorldResult GameWorld::Preprocess(void)
{
	WorldResult result = World::Preprocess();
	if (result != kWorldOkay) return (result);
 
	spectatorCamera.SetCameraAzimuth(K::pi_over_2);
 
	spectatorCamera.SetNodePosition(Point3D(0.0F, -50.0F, 16.0F));
 
	SetCamera(&spectatorCamera);
 
	Zone *root = GetRootNode();
	Node *node=root;
	do
	{
		const char *nodeName=node->GetNodeName();
		if ((nodeName) && Text::CompareText(nodeName,"Ball"))
		{
			BallController *tempBallController=TheGame->GetBallController();
			node->SetController(tempBallController);
			tempBallController->Preprocess();
		}
		else if ((nodeName) && Text::CompareText(nodeName,"Paddle1"))
		{
			PaddleController *tempPaddle1=TheGame->GetFirstPaddleController();
			node->SetController(tempPaddle1);
			tempPaddle1->Preprocess();
		}
		else if ((nodeName) && Text::CompareText(nodeName,"Paddle2"))
		{
			PaddleController *tempPaddle2=TheGame->GetSecondPaddleController();
			node->SetController(tempPaddle2);
			tempPaddle2->Preprocess();
		}
 
		node = root->GetNextNode(node);
	}while(node);
 
	return (kWorldOkay);
}
 
void GameWorld::Render(void)
{
	World::Render();
}
 
 
Game::Game() :
        Singleton<Game>(TheGame),
        displayEventHandler(&HandleDisplayEvent)
{
	TheDisplayMgr->InstallDisplayEventHandler(&displayEventHandler);
 
	prevEscapeProc = TheInputMgr->GetEscapeProc();
	prevEscapeData = TheInputMgr->GetEscapeData();
	TheInputMgr->SetEscapeProc(&EscapeProc, this);
 
	TheWorldMgr->SetWorldConstructor(&ConstructWorld);
 
	ballController=new BallController();
 
	paddleController1=new PaddleController(1);
	paddleController2=new PaddleController(2);
 
	playerOneScore=0;
	playerTwoScore=0;
 
	TheMessageMgr->BeginSinglePlayerGame();
 
	TheWorldMgr->LoadWorld("PongWorld/PongWorld");
}
 
Game::~Game()
{
	TheWorldMgr->UnloadWorld();
	TheWorldMgr->SetWorldConstructor(nullptr);
 
	TheMessageMgr->EndGame();
 
	TheInputMgr->SetEscapeProc(prevEscapeProc, prevEscapeData);
}
 
World *Game::ConstructWorld(const char *name, void *data)
{
	return (new GameWorld(name));
}
 
void Game::HandleDisplayEvent(EventType eventType, long param, void *data)
{
	if (eventType == kEventDisplayChange)
	{
 
	}
}
 
void Game::EscapeProc(void *data)
{
	TheEngine->Quit();
}
 
BallController *Game::GetBallController()
{
	return ballController;
}
 
PaddleController *Game::GetFirstPaddleController()
{
	return paddleController1;
}
 
PaddleController *Game::GetSecondPaddleController()
{
	return paddleController2;
}
 
void Game::IncreaseScore(bool player)
{
	if (player) playerOneScore++;
	else playerTwoScore++;
 
	TheEngine->Report(Text::IntegerToString(playerOneScore) + " to " + Text::IntegerToString(playerTwoScore));
}


Code Breakdown

This section attempts to explain what certain parts of the code actually do.


BallController Class

class BallController : public RigidBodyController

This is the code that controls the ball. Instead of assigning the ball a Rigid Body controller in the World Editor, this class inherits from the RigidBodyController class to provide the ball with physics.


BallController::BallController
BallController::BallController() : RigidBodyController(kControllerBall)
{
...
}


BallController::ValidNode
bool BallController::ValidNode(const Node *node)
{
...
}


BallController::Preprocess
void BallController::Preprocess(void)
{
...
}


BallController::Move
void BallController::Move()
{
...
}

The Controller class provides the Move function which is called every frame by the engine. By default, it does not perform any action. The BallController class overrides it and uses it to check the position of the ball every frame to determine if a point was scored.

Node *targetNode=GetTargetNode();
if (targetNode->GetNodePosition().x > 25)
{	
	SetRigidBodyPosition(targetNode->GetSuperNode()->GetInverseWorldTransform() * Point3D(0,0,15));
	SetLinearVelocity(Vector3D(Math::RandomFloat(6.0F)-3.0F, 0.0F, Math::RandomFloat(2.0F)-1.0F).Normalize() * 50.0F);
	TheGame->IncreaseScore(true);
}
else if (targetNode->GetNodePosition().x < -25)
{
	SetRigidBodyPosition(targetNode->GetSuperNode()->GetInverseWorldTransform() * Point3D(0,0,15));
	SetLinearVelocity(Vector3D(Math::RandomFloat(6.0F)-3.0F, 0.0F, Math::RandomFloat(2.0F)-1.0F).Normalize() * 50.0F);
	TheGame->IncreaseScore(false);
}

GetTargetNode returns a pointer to the node that the controller is attached to. GetNodePosition returns the position of the ball. Because the ball is a direct subnode of the infinite zone, these coordinates are the same as the world space coordinates of the ball. When the ball goes beyond the paddles a point is scored. SetRigidBodyPosition is used to relocate the ball back to the center of the play area, and SetLinearVelocity is used to set the new velocity for the ball. When setting the velocity, the RandomFloat function is used to generate a random velocity vector. The Normalize function simply takes the random vector and makes makes it length one, which in this case is used to ensure a consistent velocity of 50 meters/sec for the ball by multiplying by 50.0F.


PaddleController Class

class PaddleController : public Controller

This is the code that controls the paddles. This class inherits from the generic Controller class, which is the basis for every controller in C4.


PaddleController::PaddleController
PaddleController::PaddleController(long player) : Controller(kControllerPaddle)
{
 
}


PaddleController::Preprocess
void PaddleController::Preprocess(void)
{
...
}


PaddleController::Move
void PaddleController::Move(void)
{
...
}

Similar to the BallController::Move function, the PaddleController class overrides the Move function of the Controller class to use it to update the paddles every frame.


GameWorld Class

class GameWorld : public World

This class is used when a world is loaded from a file. In this series, it is used to create a stationary camera and to assign the paddle and ball controllers to their respective geometries.


GameWorld::Preprocess
WorldResult GameWorld::Preprocess(void)
{
...
}


Zone *root = GetRootNode();
Node *node=root;
do
{
	...	
	node = root->GetNextNode(node);
}while(node);

This snippet of code retrieves the root node in the world (typically the infinite zone), and iterates through all of the nodes in the world.

const char *nodeName=node->GetNodeName();
if ((nodeName) && Text::CompareText(node->GetNodeName(),"Ball"))
{
...
}

As we're iterating through the nodes in the world we look at their names and compare them to strings we expect. It's important to remember that unless all of the nodes in the world are given a name, we need to check and make sure the node has a name before using the Text::Compare(...) function, otherwise it will crash when you try to compare the name of a node that has no name.

BallController *tempBallController=TheGame->GetBallController();
node->SetController(tempBallController);
tempBallController->Preprocess();

Once we determine that we've found a node we're looking for, we create a controller and attach it to the node.


Game Class

class Game : public Singleton<Game>, public Application

This is the heart of the game. It inherits from the Application class and is expected by the engine.


Game::IncreaseScore
void Game::IncreaseScore(bool player)
{
...
}

This function is used to update and display the current score for the game.

if (player) playerOneScore++;
else playerTwoScore++;
 
TheEngine->Report(Text::IntegerToString(playerOneScore) + " to " + Text::IntegerToString(playerTwoScore));

This is called in the BallController::Move(void) method every time a player scores. It increments the score for the player that scored, and then prints it out to the engine's command console using the Report function available through the TheEngine global pointer. The scores are stored as integers, and in order for them to be printed out to the console, they are converted to text using Text::IntegerToString(...).


Game::EscapeProc
void Game::EscapeProc(void *data)
{
...
}

This function is set to be called when the escape key is pressed.

TheEngine->Quit();

By simply adding TheEngine->Quit(), the game will close when the escape key is pressed. If the command console is visible, then it will actually close the console instead of the entire application.


Game::Game
Game::Game() :
        Singleton<Game>(TheGame),
        displayEventHandler(&HandleDisplayEvent)
{
...
}


TheWorldMgr->LoadWorld("Pong");

This command simply demonstrates how to load a world from file. In this case, the world is located at /data/somefoldername/Pong.wld.


TODO

  • Finish the rest of the explanations


Series Main Page: Pong Series

Personal tools