Game Development Community

A non-FPS Torque game: TicTacToe

by Thijs Sloesen · 01/27/2004 (9:20 pm) · 53 comments

Download Code File

A non-FPS Torque game: TicTacToe

Introduction
Every now and then I read forum posts asking for examples of non-FPS games. The people posting these questions argue that the Torque SDK is primarily focused on creating first person shooter games and not games of other genres like board games, card games or even (realtime) strategy games. While the examples that are shipped with the SDK are indeed for the most part focused on first-person games, it is very well possible to create games that are not even close to an FPS using the Torque SDK.

This tutorial tries to explain how to create a game that is totally different from the "FPS" examples that ship with the SDK. This tutorial describes the making of a small Torque game: Tic Tac Toe. You'll see how to set up a new client-side mod which we'll then use to implement the game. Not only will we script, but we will also add a bit of functionality to the engine itself. Note that this is a client-side mod with no network features. When I find the time I'll try to write another tutorial which addresses issues like sound, multiplayer and removing the "common" and "starter.fps" mods from the game. For now, this example shows how to set up a new game project from scratch. The code presented here is not optimized to allow for easy reading.

The code in this tutorial is based heavily on the show mod - which is included with the SDK - and on my understanding of the Torque source code. Since as of writing I do not have a lot of experience with Torque yet, some facts and examples could be wrong or incomplete. Please post any additions, rectifications or recommendations as comments to this article. Thank you in advance.

Note: while I created the source files in Linux, this example should run on Windows and Macintosh just as well since only the Torque API is used and no platform-specific calls. Even though in this tutorial we build the engine using the "make" program, it could also be done using Visual Studio, or whatever you use :). The code itself is written for the 1.2 release of Torque.

1. A rendering control
Let's jump right into the engine source! First we will create a rendering control. The rendering control will be responsible for rendering our 3D scene. Because we will do our own rendering, we are going to create a custom class derived from the GuiTSCtrl class. This class will extend the Torque engine binary.
Create the following two files in your Torque codebase:
engine/game/tttTSCtrl.h :
#ifndef TTTTSCTRL_H
#define TTTTSCTRL_H

#include "gui/guiTSControl.h"

class tttTSCtrl : public GuiTSCtrl {
   typedef GuiTSCtrl Parent;
public:
   bool processCameraQuery(CameraQuery *query);
   void renderWorld(const RectI &updateRect);

   DECLARE_CONOBJECT(tttTSCtrl);
};

extern void tttInit();

#endif // TTTTSCTRL_H
engine/game/tttTSCtrl.cc :
#include "../game/tttTSCtrl.h"

IMPLEMENT_CONOBJECT(tttTSCtrl);

bool tttTSCtrl::processCameraQuery(CameraQuery * q) {
   return true;
}

void tttTSCtrl::renderWorld(const RectI &updateRect) {
	
}

void tttInit() {

}

From the Torque documentation:
Quote:GameProcessCameraQuery() returns the viewing camera of the current control object (the object in the simulation that the player is currently controlling), then GameRenderWorld makes the client scene graph object render the world.

Next, we need to make sure our code gets compiled and built into the actual game binary, so in engine/targets.torque.mk, we change the lines
SOURCE.GAME=\
game/main.cc \
game/debris.cc \
(...)
to
SOURCE.GAME=\
game/main.cc \
game/tttTSCtrl.cc \
game/debris.cc \
(...)
This makes sure that our newly created source file gets compiled when you make the engine. Note: when working with Visual C++, all you have to do is add the files tttTSCtrl.h and tttTSCtrl.cc to the project. Just to make sure everything works, compile the engine:
Quote:epidemi@host torque $ make
--> Compiling game/main.cc
--> Compiling game/tttTSCtrl.cc
--> Linking out.GCC3.RELEASE/torqueDemo.bin
epidemi@host torque $
We now have added our own class to the engine and it is available for use in our scripts.

2. Let's start modding
We will be implementing a tiny part of our game in scripts, so let's start off by creating a directory for our game under the "example" directory in the Torque codebase called "ttt". Create another directory called "ttt/ui". In the "ttt" directory, we create a file called main.cs:
//-----------------------------------------------------------------------------
// Torque Game Engine 
// Copyright (C) GarageGames.com, Inc.
//
// TicTacToe Mod
// Copyright (C) 2004 Thijs Sloesen
//-----------------------------------------------------------------------------

// start the game
// - initialize gui
// - show menu
//
// called by onStart()
function startTicTacToe() {
  
}

//-----------------------------------------------------------------------------
// Package overrides to initialize the mod.
// This module currently loads on top of the client mod, but it probably
// doesn't need to.  Should look into having disabling the client and
// doing our own canvas init.
package TicTacToe {
function onStart() {
   Parent::onStart();
   echo("\n--------- Initializing MOD: TicTacToe ---------");
   if (!isObject(Canvas))  {
      // If the parent onStart didn't open a canvas, then we're
      // probably not running as a mod.  We'll have to do the work
      // ourselves.
      initCanvas("TicTacToe");
   }
   startTicTacToe();
}
}; // TicTacToe package

activatePackage(TicTacToe);
As you can see, this code skeleton is based on the show mod. When the game launches (when the mod is invoked) the package TicTacToe is activated and the function onStart() is called. This function calls startTicTacToe() to initialize and start the game. The bodies for these functions are still empty but we'll work on that a little later.

For now, let's verify that everything is working by starting Torque and loading our mod:
Quote:epidemi@host example $ ./torqueDemo.bin -nohomedir -mod ttt
epidemi@host example $
Note that we specify the "-nohomedir" parameter to prevent Torque redirecting all file I/O to ~/.garagegames/torqueDemo, as MangoFusion pointed out to me in #garagegames.

Torque should popup with the FPS Starter Kit menu. To make sure your mod loaded successfully, check the console by pressing the tilde key on the keyboard. It should say something like "Initializing MOD: TicTacToe".

3. We need a GUI!
Ok, we have a working MOD. Now what? In chapter 1, we created a custom rendering control. It's time to put that control to work. Open the GUI editor by pressing F10. Open the File menu and select "New GUI...". Enter "TicTacToeGui" as GUI name and "GuiControl" as class. Press the "New control" button and create a new tttTSCtrl control. This creates a new GUI control that is based on our freshly-made C++ GUI control class. Now open the File menu again, and select "Save GUI...". Choose "ttt/ui" as directory and save the GUI as "TicTacToeGui.gui". Press F10. As you will notice, The screen does not get cleared and moving the mouse ensures the screen leaves a nice cursor trail.

To fix this, quit Torque and open up the file tttTSCtrl.cc again. Change the function tttTSCtrl::renderWorld to resemble the code listed below:
void tttTSCtrl::renderWorld(const RectI &updateRect) {
    glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
    dglSetClipRect(updateRect);
}

This will clear the depth buffer and the drawing buffer each frame and set up a clipping rectangle.

We also need to edit our script a bit so that it will load the GUI script when it is started. So in main.cs, change the function startTicTacToe() to resemble the following code listing:
function startTicTacToe() {
   exec("~/ui/TicTacToeGui.gui");
   Canvas.setContent(TicTacToeGui);
   Canvas.setCursor("DefaultCursor");   
}

Compile Torque again, and view the result by loading our mod. If all went well you will now have a black background again, but the cursor trail is gone: the screen gets cleared and repainted each frame.

Ok, back to our GUI. Add a button with the caption "Start new game". Set the command to "startNewGame();". Add another button with the caption "Quit game" and with the command "quit();". And in our C++ codefile tttTSCtrl.cc we add a console function called startNewGame():
ConsoleFunction( startNewGame, void, 1, 1, "" ) {
}
This function will be called when the user presses the start new game button.

Time for some game logic. Let's create a C++ class called "tttGame" which implements the following interface:
engine/game/tttGame.h :
class tttGame {
public:
	enum {
		PLAYER_NONE 		= 0,
		PLAYER_1,
		PLAYER_2,
		PLAYER_END
	};

	tttGame();
	~tttGame();
	bool startGame(unsigned int startingPlayer); // start the game
	bool endGame(); // end the game
	bool makeMove(unsigned int x, unsigned int y); // place a stone on the board
	bool gameOver() const; // is the game game over?
	unsigned int whoWon() const; // return winner
	unsigned int getPlayer() const; // return current player
	unsigned int getCellState(unsigned int x, unsigned int y); // return state of cell on board
};
This class will control the game logic and will supply the rendering class (tttTSCtrl) of it's data concerning the location of the pieces on the tic tac toe board. I'll leave the implementation of this class up to your imagination, but if you are in a hurry, the download that accompanies this tutorial contains a sample implementation.

4. Camera, models, rendering, input, action!
Against better knowledge we add a global variable to our C++ file :
tttTSCtrl *curTTTCtrl = NULL;
We also copy the body of the processCameraQuery() member function from the show mod:
bool tttTSCtrl::processCameraQuery(CameraQuery * q) {
	MatrixF xRot, zRot;
	xRot.set(EulerF(tttCamRot.x, 0, 0));
	zRot.set(EulerF(0, 0, tttCamRot.z));
	
	tttCameraMatrix.mul(zRot, xRot);
	q->nearPlane = 0.1;
	q->farPlane = 2100.0;
	q->fov = 3.1415 / 2;
	tttCameraMatrix.setColumn(3, tttCamPos);
	q->cameraMatrix = tttCameraMatrix;
	
	return true;   
}
This will make sure the engine has the right information about the camera when it wants to render a frame.

For our own comfort, we add two simple utility member functions to our class:
void tttTSCtrl::translate(float x, float y, float z) {
	lastTranslation = Point3F(x, y, z);
	glTranslatef(x, y, z);
}

void tttTSCtrl::untranslate() {
	glTranslatef(-lastTranslation.x, -lastTranslation.y, -lastTranslation.z);
	lastTranslation.x = lastTranslation.y = lastTranslation.z = 0.0f;
}
The first function merely translates (moves) over the given x, y and z coordinates and the second function actually undoes this translation. Don't forget to add the lastTranslation (Point3F) member variable to the class. And before we forget, we need to change the constructor and destructor of our class to the following:
tttTSCtrl::tttTSCtrl(): boardShapeInstance(NULL), 
						roundStoneShapeInstance(NULL), 
						crossStoneShapeInstance(NULL) {
	// FIXME: make singleton
	curTTTCtrl = this;
							
	// load our artwork							
	hBoardShape = ResourceManager->load("ttt/data/shapes/board/board.dts");	
	if (bool(hBoardShape)) {	
		boardShapeInstance = new TSShapeInstance(hBoardShape, true);
	}
	hRoundStoneShape = ResourceManager->load("ttt/data/shapes/board/stone-round.dts");	
	if (bool(hRoundStoneShape)) {	
		roundStoneShapeInstance = new TSShapeInstance(hRoundStoneShape, true);
	}
	hCrossStoneShape = ResourceManager->load("ttt/data/shapes/board/stone-cross.dts");	
	if (bool(hCrossStoneShape)) {	
		crossStoneShapeInstance = new TSShapeInstance(hCrossStoneShape, true);
	}
	
	// Set up our camera
	tttCameraMatrix.identity();
	tttCamRot.set(0.0f,0.0f,0.0f);		
	tttCamPos.set(0.0f,-4.0f,0.0f); // board object	
	tttCameraMatrix.setColumn(3,tttCamPos);

	// I couldn't get unproject() and dglPointToScreen() to work,
	// so here's a hack that will have to do:	
	// hardcoded board coordinates on screen for now
	boardOnScreen.point.x = 183;
	boardOnScreen.point.y = 100;
	boardOnScreen.extent.x = 585;
	boardOnScreen.extent.y = 500;
							
}

tttTSCtrl::~tttTSCtrl() {
	// Clean up our art
	hBoardShape.unlock();
	hRoundStoneShape.unlock();
	hCrossStoneShape.unlock();	
	delete boardShapeInstance;
	delete roundStoneShapeInstance;
	delete crossStoneShapeInstance;
	curTTTCtrl = NULL; // Should not point to this instance anymore
}
We want to make sure that our console script function startNewGame() is able to access our rendering control. It might be possible to iterate through the GUI controls using a script and effectively rendering out this step, but I'm not sure about that so for now this will have to do.

And for the art-related code to work we need a couple of private member variables:
RectD boardOnScreen;

Point3F tttCamPos;
MatrixF tttCameraMatrix;
EulerF  tttCamRot;

Resource<TSShape> hBoardShape;
Resource<TSShape> hRoundStoneShape;
Resource<TSShape> hCrossStoneShape;

TSShapeInstance* boardShapeInstance;
TSShapeInstance* roundStoneShapeInstance;
TSShapeInstance* crossStoneShapeInstance;

Ok, now we have only 3 more pieces of code to write:
1. the code that starts the game
2. the code that responds to user input (ie. Mousedown event)
3. the code that actually renders our 3D scene

First off, the code that starts the game. We create a member function tttTSCtrl::startGame() as follows:
void tttTSCtrl::startGame(unsigned int startingPlayer) {	
	// Make sure our artwork is loaded
	AssertFatal( boardShapeInstance != NULL, "Could not load board shape!" );
	AssertFatal( roundStoneShapeInstance != NULL, "Could not load round stone shape!" );
	AssertFatal( crossStoneShapeInstance != NULL, "Could not load cross stone shape!" );
	
	game.startGame(startingPlayer);
	Con::printf("Player %d has a move", game.getPlayer());
}
And we adjust our console function accordingly:
ConsoleFunction( startNewGame, void, 1, 1, "" ) {
	AssertFatal( curTTTCtrl != NULL, "Instantiate a tttTSCtrl object!" );
	curTTTCtrl->startGame(tttGame::PLAYER_1);
}
This ensures that the code under our "start new game" button can call the C++ code to initialize and start a new game.

We now need some code that deals with mouse input and controls the game accordingly:
void tttTSCtrl::onMouseDown (const GuiEvent& event) {
	if (game.gameOver())
		return;
	
	// Find out if the user clicked on the board
	if ((event.mousePoint.x >= boardOnScreen.point.x) &&
		(event.mousePoint.x <= boardOnScreen.extent.x) &&
		(event.mousePoint.y >= boardOnScreen.point.y) &&
		(event.mousePoint.y <= boardOnScreen.extent.y)) {
		// Yes, they did click on the board
		Point3F pointRel;
		float x, y, w, h;
		// Determine which cell the user clicked on
		pointRel.x = event.mousePoint.x - boardOnScreen.point.x;
		pointRel.y = event.mousePoint.y - boardOnScreen.point.y;
		w = boardOnScreen.extent.x - boardOnScreen.point.x;
		h = boardOnScreen.extent.y - boardOnScreen.point.y;
		x = pointRel.x / w * 3;
		y = pointRel.y / h * 3;
		//Con::printf("Selected: %d, %d", (int)x, (int)y);
			
		if (game.makeMove((int)x, (int)y)) {
			Con::printf("Player %d has a move", game.getPlayer());	
		}	
		else {
			Con::printf("Invalid move");		
		}
		if (game.gameOver()) {
			Con::printf("Game over!");
			switch(game.whoWon()) {
				case tttGame::PLAYER_1:
					Con::printf("Player 1 won!");
					break;
				case tttGame::PLAYER_2:
					Con::printf("Player 2 won!");
					break;
                                default:
					Con::printf("It's a tie!");
					break;
			}
		}
	}
}
This code should speak for itself.

And last, but not least, the rendering code:
void tttTSCtrl::renderWorld(const RectI &updateRect) {
	glClearColor(0.2f, 0.3f, 0.5f, 0.0f);
	glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

	glDisable(GL_CULL_FACE);
	glMatrixMode(GL_MODELVIEW);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	dglSetCanonicalState();

	if (boardShapeInstance) {
		boardShapeInstance->render();

		translate(-1.0f, 0.0f, 1.0f); // move to center of board
		int x, y;
		for (y=0;y<3;++y) {
			for (x=0;x<3;++x) {
				// Use sine to correctly position the stones	
				translate( x - 0.3f * sin(((x + 0.5f) / 3.0f) * 2.0f * 3.1415),
						   0,
						  -y + 0.3f * sin(((y + 0.5f) / 3.0f) * 2.0f * 3.1415)
						  );
				if (game.getCellState(x, y) == tttGame::PLAYER_1)
					roundStoneShapeInstance->render();
				if (game.getCellState(x, y) == tttGame::PLAYER_2)
					crossStoneShapeInstance->render();
				untranslate();
			}
		}
	}

	glDisable(GL_DEPTH_TEST);	
	dglSetClipRect(updateRect);
}
This code simply renders the board, iterates through all cells on the board and renders a stone if one is set for each cell.

Well, this is as far as my humble Tic Tac Toe tutorial goes. Just hope I covered every line of code :) As you can see, I'm still learning a lot about TGE, but by sharing my experiences I hope some of you can actually speed up their own development. Any ideas and comments are welcome, and when I have some time to spare I'll try to write a tutorial on extending this simple game towards something more mature (ie. Netplay, sound, etc) as as you can see the game in its current state is nowhere near production level.

Rests me to say "Thank you!" to all you GG members out there (and of course the GG staff!) that help each of us move forward in this great community.

Regards,

Thijs
Page «Previous 1 2 3 Last »
#1
01/27/2004 (9:57 pm)
Very nice writing, surely will help people that want to really understand Torque.
#2
01/27/2004 (10:30 pm)
Cool.

It actually opened up some new options for me.

Thanks for putting this up.

:)
#3
01/28/2004 (12:33 am)
I found an article up here that went over configuring a new project using torque in VC6, I haven't been able to find it and really want to look @ it. If someone knows of any such tutorial or forum post could you please respond to this post.


Thanks in advance.
#4
01/28/2004 (12:48 am)
And of course excellant tutorial I had no problems following it at all. It also helped me to understand the interaction between the scripts and C++ a little better!. so thank you. :}
#5
01/28/2004 (4:34 am)
This looks great. I've had torque about a year now, but I've never really had much time to go digging into the actual engine code and make completely new features, only slight modifications. I'm sure this tutorial will be a great help as a reference for me to get into that. Thanks!
#6
01/28/2004 (7:15 am)
Thanks for your feedback people! I actually learned a couple of things while writing this tutorial and reading through the SDK sources. Just thought it would be nice to share the knowledge :)

Frederick: Is this the tutorial you're looking for? www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=4562
#7
01/28/2004 (9:53 am)
Seems like a really good resource, thank you Thijs.

PS: Link doesn't seem to work yet.
#8
01/28/2004 (12:13 pm)
Awsome resource. I'm guessing you can't incrementally add the code along with the tutorial. I got to #3 and when I tried to add the tttTSCtrl I got a Fatal Assert "MatrixF::inverse non-singular matrix, no inverse". Hopefully the Link we be working soon.

After adding in the code for tttTSCtrl::processCameraQuery() it seems to work now.
#9
01/28/2004 (1:33 pm)
Owen: I have seen that error too while I was working on this tutorial. But I couldn't reproduce it. Guess I screwed up a bit somewhere in the order of which the code should be added. I'll check it out when I have some more time.

Anyway, until GG gets the link working, I've mirrored the source here.

And thanks for your comments, folks :)
#10
01/28/2004 (9:04 pm)
Thank you Thijs!!. Thats the one. :}

Abd BTW this tutorial rocks!. A really enjoyed it. It really helped me understand the interaction between the scripts and the C++ side. Thanks for submitting it. Im working on adding Network and Lobby functionality for it and will post it as soon as I get it done
#11
01/30/2004 (3:26 am)
Thanks :)

Good idea! I'm looking forward to seeing it!
#12
01/30/2004 (3:41 am)
We needed this type of tutorial since the begining! Thanks!
#13
01/30/2004 (11:58 pm)
This is one of the best tutorials that's very useful for me. Thanks!
#14
02/03/2004 (10:15 am)
Tres cool. Thanks!
#15
02/05/2004 (10:09 am)
Great! This is what I've been waiting for! Thanks!
#16
02/06/2004 (10:46 pm)
i'm having a tad of trouble understanding the math part of the tutorial.. could someone put some real world values in for me.. or something.. to help me understand these parts..


((event.mousePoint.x >= boardOnScreen.point.x) &&
(event.mousePoint.x <= boardOnScreen.extent.x) &&
(event.mousePoint.y >= boardOnScreen.point.y) &&
(event.mousePoint.y <= boardOnScreen.extent.y))

and

translate( x - 0.3f * sin(((x + 0.5f) / 3.0f) * 2.0f * 3.1415),0,-y + 0.3f * sin(((y + 0.5f) / 3.0f) * 2.0f * 3.1415));

and sine one will probly be harder to explain as i havnt taken a math class covering it in school (i'm taking geometry and honors algebra 2 this year..)
#17
02/07/2004 (4:51 am)
Hi,

the first part, namely:

((event.mousePoint.x >= boardOnScreen.point.x) &&
(event.mousePoint.x <= boardOnScreen.extent.x) &&
(event.mousePoint.y >= boardOnScreen.point.y) && 
(event.mousePoint.y <= boardOnScreen.extent.y))

is actually quite simple. What we do here is check if the mousebutton was pressed in side the (square) area on the screen where the TTT board is rendered. the variable boardOnScreen holds the values of the rectangle that contains the TTT board on the 2D screen:

epidemi.seekitfast.com/downloads/tictactoe-area.jpg
This variable is initialized in function tttTSCtrl::tttTSCtrl().

Your second question is indeed a bit tougher to explain, but I'll try anyway :)

What we do here is draw the TTT pieces on the board. The piece that is in the middle of the board is drawn at the center of the board.

The way we would like to draw the pieces now is using the following code:

translate(x, 0, -y);
// render here

Unfortunately, as you can see in the following screenshot, this does not give the desired effect:

epidemi.seekitfast.com/downloads/tictactoe-gonebad.jpg
The center piece is rendered correctly, the pieces around it are misplaced and positioned too much to the center. What we need is some way to alter our (x, y) pairs in such a way that they all just move a little bit away from the center.

About the sine function: as you can see on this webpage the sine function is a periodical function that has the following values:

if x = 0 then y = 0
if x = 0.5 * PI then y = 1
if x = PI then y = 0
if x = 1.5 * PI then y = -1
if x = 2 * PI then y = 0

We use the sine function here to add or substract a bit to/from our coordinates. So in fact, for each piece we have:

translate(x + something, 0, -y + something);

So we use the periodical property of the sine to add or substract a bit from our coordinates so we display them correctly. Note that the "+ 0.3f" and (/ 3.0f) in the formula is used much like a percentage; for our first stone we need 1/3rd (30%) of the sine value, for our second stone we need 2/3rd (60%) etc.

Phew... I hope this makes any sense to you. If not, you might want to consider reading up on the sin, cos and tan functions as they can be quite handy now and then :)

Regards,

Thijs
#18
02/07/2004 (2:13 pm)
Thx alot, man.. the pics really help..

I appreciate you talking the time to help me..

one more question, how does OnMouseDown() send the x,y cords to renderWorld()?
#19
02/07/2004 (3:16 pm)
Hi again :)

The OnMouseDown() function tells our "game" class about the position of the click on the board with the following call:

game.makeMove((int)x, (int)y)

and the RenderWorld() function actually retrieves information about which pieces need to be drawn by means of the function call:

game.getCellState(x, y)

As you can see, the tttGame class is actually used to store the information about the mouse clicks and which pieces need to be drawn.

Hope this helps.

Regards,

Thijs
#20
02/07/2004 (8:52 pm)
I'm having a little trouble with my code.. i'v kinda broken away from your tutorial..

http://hdcwargame.com/TotalControl/tttTSCtrl.cc
http://hdcwargame.com/TotalControl/tttTSCtrl.h
theres the code..

I'm wanting to put down a cross were ever i click.. and it kinda half ass works.. one.. the cross is super small.. like a few pixels small.. and 2.. its offset a bit.. so if i click at 5,5.. the cross will be at 8,8.. also i think there is something wrong with the x,y cords.. becuase the cross just kinda stays in the center and only moves left or right.. so if you move the mouse down or to the right and click.. the cross moves to the right.. if you move it up or left.. the cross moves left.. very wierd..

any help would be appreciated.. thx..
Page «Previous 1 2 3 Last »