Brass SSE Developer Guide - Tutorials
Tutorial 6 - Adding Game Logic
and using Functions
Grid? Check. Images? Check. Grid state stored? Check.
Display the symbols? Check. We're nearly there, all we need now
is to let the player click on the grid to make their move.
Download the source
for this tutorial
Back to Input Handling
By now you could probably write this code yourself
without help, so let's write the input handling code fairly quickly
so we can get onto more interesting things. The human player will
play as X and the plugin-computer-player will play as O.
There are 2 conditions we need to handle: first, the
user clicking in the grid and second, the user clicking outside
the grid. That's our "top level" condition.
If the user clicks in the grid we need to figure out
what square they clicked on and check its status. If it's empty
(FREESQUARE) then we need to set the grid array element for that
square to be XSQUARE - the user's symbol. If it's not empty we just
ignore the click until the user visits ye olde clue shoppe :)
Once the user has made their move the plugin needs
to make a move too. You could make this code as smart as you like
with proper logic to figure out the best move, but for now we're
just going to get our code to make a move in the first free square
it finds in the array.
Enough theory, let's get coding.
The OnMouseLButtonUp Handler
Let's go back to our old OnMouseLButtonUp handler.
At the moment we've still got some code from way back in tutorial
3 in there. Let's get rid of that because we don't need it any more.
Remove the PosX = iX; and
PosY = iY; lines of code from the handler
Delete the global PosX and
PosY declarations at the top of your source
Code time. Task 1 - check the click was inside the
grid. To do that we need to have access to the grid position, width
and height in the OnMouseLButtonUp handler.
Add the following global declarations
to the top of your code:
int global XSQUARE =
1;
int global OSQUARE = 2;
// The grid size
declare global iGridTopX int;
declare global iGridTopY int;
declare global iGridWidth int;
declare global iGridHeight int;
Change the start of the Render
handler code to remove the variable declarations:
function Render()
{
int iWidth = getpanelwidth();
int iHeight = getpanelheight();
// Setup
the grid position and size
iGridTopX
= 10;
iGridWidth
= iWidth - iGridTopX - 10;
iGridTopY
= iHeight / 4;
iGridHeight
= iHeight / 2;
The grid size and position is now global, so we can
test for a click inside it in the OnMouseLButtonUp handler.
Add the following code to
the OnMouseLButtonUp handler:
function OnMouseLButtonUp(int iX, int iY)
{
if(iX >
iGridTopX and iY > iGridTopY and
iX < iGridTopX + iGridWidth and
iY
< iGridTopY + iGridHeight)
{
//
Store the size of grid 1/3rd's
int
iGridThirdW = iGridWidth / 3;
int
iGridThirdH = iGridHeight / 3;
//
Set the click to zero based index
iX
= iX - iGridTopX;
iY
= iY - iGridTopY;
//
Calculate the element clicked on
int
iElementClicked = iX / iGridThirdW;
iElementClicked
= iElementClicked + ((iY / iGridThirdH) * 3);
//
Click was on an empty square?
if(GridStatus[iElementClicked]
== FREESQUARE)
{
//
Yes!
GridStatus[iElementClicked]
= XSQUARE;
//
Do the computer move
int
iGridElement = 0;
bool
bFoundFreeSquare = False;
//
Loop while the conditions are true
while(iGridElement
< 9 and bFoundFreeSquare == False)
{
//
Is this a free square?
if(GridStatus[iGridElement]
== FREESQUARE)
{
GridStatus[iGridElement]
= OSQUARE;
//
Flag we want to exit the loop
bFoundFreeSquare
= True;
}
iGridElement++;
}
}
}
redraw();
}
The code in the sample download for this tutorial
commented in a lot of detail so it's a good idea if you download
and read through it. This code is straightforward, there's just
a little bit more of it needed to calculate where the click was,
and to make a computer-move.
That's all the logic we need for the game to work,
time to test it out!
Click the "Compile" icon (
)on the toolbar
Click the "Execute" icon (
) on the toolbar
Reusable Code in Functions
If you've played the game you just coded you'll notice
a pretty serious limitation. Once you've filled up the grid squares
and it's game over, it really is game over. Unless you unload and
reload the plugin you can't play another game. What we need to do
is to reset the grid once it's been filled.
We already have the code to reset the grid status
array in our Init handler, it's this code:
for(int iGridElement = 0; iGridElement <
9; GridElement = iGridElement +
1)
{
// Set
each element to a free square
GridStatus[iGridElement]
= FREESQUARE;
}
The problem is, we need this code in the mouse handler
because that's where we know when the last move has been made. We
could just copy and paste this code into the mouse handler but that's
very messy. Duplicating code isn't a good idea because if you need
to make a change in future (maybe to use to a 6*6 grid instead)
you have to remember all the places you pasted the array reset code.
You might be wondering why we don't just add
the reset code to the Render handler. This would actually work
perfectly, but it's not very efficient. We're adding more code
to the Render handler that has to be executed every time the
plugin redraws its display. If we add the reset code to the
mouse handler we can make sure we only reset the array when
the last mouse click (the one that completes the grid) is received.
It's much better to put the code in a function and
call it whenever we need it.
C coders! Unlike C, Shiny allows use of functions
without declaring/prototyping them first. You can quite happily
call a function on line 1 of your code and not implement it
until line 100. Smarter than a C compiler, that's us ;)
In Shiny, functions look exactly like handlers. Creating
them is very easy - just pick a name for your function and declare
it like this:
function ResetGridStatus()
{
// Function
code goes in here
}
To use the function (execute the code inside it),
simply call it like this:
// Now reset the grid status
ResetGridStatus();
Simple! Let's move the array reset code into a function
first.
Create a new function declaration
at the bottom of your source code, like this:
function ResetGridStatus()
{
}
Move the array reset code
from the Init handler to the new function:
function ResetGridStatus()
{
for(int
iGridElement = 0; iGridElement < 9;
iGridElement
= iGridElement + 1)
{
//
Set each element to a free square
GridStatus[iGridElement]
= FREESQUARE;
}
}
In the Init handler add a
function call where the array reset code was:
createimage(SymbolX,
"SSE\\TicTacImages\\tictac-x.gif")
createimage(SymbolO, "SSE\\TicTacImages\\tictac-o.gif");
ResetGridStatus();
Although we moved some code around we didn't change
the logic of the game. Build and test the plugin just to confirm
everything still works.
Click the "Compile" icon (
), then the "Execute" icon (
) on the toolbar
Handling Game Over
Now that we can call our array reset function at any time, we can
add some code to the OnMouseLButtonUp handler to call it when the
grid is full. So how do we know when the grid is full?
Here's a segment of the code we added to the OnMousLButtonUp handler
earlier in the tutorial.
bool bFoundFreeSquare = False;
// Loop while the conditions are true
while(iGridElement < 9 and bFoundFreeSquare == False)
{
// Is this a free
square?
if(GridStatus[iGridElement]
== FREESQUARE)
{
GridStatus[iGridElement]
= OSQUARE;
//
Flag we want to exit the loop
bFoundFreeSquare
= True;
}
iGridElement++;
}
(it starts on line 149 of the sample code download
for this tutorial).
If you look closely at this loop you can see that
the bFoundFreeSquare variable is used
to track whether (unsurprisingly) a free square was found in the
grid. If one wasn't found it remains set to False.
So to check if there were any free squares left in
the grid all we need to do is to check the bFoundFreeSquare
variable after the while loop has
finished. If it equals False, we call our grid reset function to
start the game again.
Add the following code after
the end of the while loop:
iGridElement++;
}
// No free squares? Reset the grid for
another game
if(bFoundFreeSquare == False)
ResetGridStatus();
If you test the code now, when the last click is made
in the last free grid square the game is reset and we can play again.
Click the "Compile"
icon (
), then the "Execute" icon (
) on the toolbar
Okay, so it's not 100% user friendly because the grid
is wiped immediately. If we were going to develop a full blown game
then we'd add a variable that tracks if the game is over and probably
waits for another click to signify the user wants to restart the
game. We could even add an image of a button saying "Reset
Game" that resets the grid no matter when the user clicks on
it. As a learning exercise you should add some features to the game
to make sure you understand all the principles we've covered so
far.
A Little More on Functions
Although this isn't related to the game it's a good
time to discuss how to use the full range of function features.
So far we've seen how to call a function that performs all its actions
internally, but what about a function that acts on some values?
Imagine that adding 2 to a number was a very long
and complex operation, and we needed to do it many times in our
code. The obvious solution is a function that reuses the same addition
code, but how do we get it to add 2 to a number?
The answer is function parameters. Parameters are
values you pass to a function so that it can use them in whatever
code it needs to. It means we don't need loads of global variables
just to get data moving between parts of our code. Here's how to
define a function that accepts a parameter of one integer:
function AddTwo(int Victim)
{
Victim
= Victim + 2;
}
To call this function simply add the value into the
function call brackets:
// Add 2 to 1
AddTwo(1);
// Add 2 to the value of MyVal
AddTwo(MyVal);
To accept more than one parameter in a function simply
add another declaration separated by a comma:
function AddTwo(int Victim, string Message)
{
Victim
= Victim + 2;
drawtext(Message,
0, 0);
}
And to call it just add another value or variable
to the call:
// Add 2 to 1
AddTwo(1, "Now adding 2 to 1");
// Add 2 to the value of MyVal
AddTwo(MyVal, "Now adding 2 to MyVal");
All this code has one small problem though. When a
function is doing something "internally" (eg: our ResetGridStatus
function did all the work inside itself and didn't need to accept
parameters or provide any new values) then we're ok. But what if
we need our function to give us something back? Everything in the
function is local scope (scope was covered in tutorial
3) so unless we make a global variable we can't get any data
out of it. And we just said that we didn't want to use lots of global
variables.
Wouldn't our AddTwo function be far more useful if
it returned the result of adding 2? Sure it would, and it's very
easy to do. We simply declare the type of variable our function
returns before the function declaration,
like this:
int function AddTwo(int Victim)
{
}
Then to return the result of the operation we add
a return statement to the end of the
function body:
int function AddTwo(int Victim)
{
Victim
= Victim + 2;
return
Victim;
}
The return statement
says, "make this value available to the statement that called
this function". Call a function that returns a value like this:
int iMyValPlus2 = AddTwo(MyVal);
It is perfectly fine to call a function that returns
a value without doing anything to it. For example, this is
legal:
AddTwo(MyVal);
Even though AddTwo()
returns an integer and we're not assigning it to anything, this
is perfectly legal. You don't always have to store or act on a return
value. This is useful if you sometimes want to ignore an error check:
function Init()
{
SetupStuff();
}
bool function SetupStuff()
{
if(Today
= "Monday")
{
return True;
}
else
{
return
False;
}
}
Here we're calling the SetupStuff function and ignoring
the return value.
Although you can ignore returned values when you call
a function, the function itself must always obey its return
type. For example, if you declare a function like this:
string function SetupStuff()
{
}
You must include a return
statement that returns a string, like this:
string function SetupStuff()
{
return
"Any old string";
}
If you forget to include a return statement the compiler will generate
an error:
(Line 39) Function
requires a return statement
If you return an incompatible type, or forget to return
a type at all like this:
string function SetupStuff()
{
return;
}
The compiler will generate an error like this:
(Line 39) Function
contains incompatible return type statements
Something a little curious
All this talk of returning variable types brings up
another important part of Shiny. Add the following function to your
code (it doesn't matter where, this is just to demonstrate something
- you can delete it right after):
string function ThatsOdd()
{
return
99;
}
Compile the code (don't bother executing it). You're
going to get a compiler error, right? We declared the function as
one that returns a string, and we tried to return a number. Just
a few lines above we learned about incompatible return types, so
there's no way this is going to work...
0 error(s) were found
Er, what? We're returning an integer
in a function that has a return type of string.
How can that possibly work?
Good question, and it's all part of the Shiny language.
You'll find out the answer in the next tutorial!
A Quick Recap
This tutorial has seen us complete the fundamentals
of the tictactoe game, so from now on everything is just adding
features and frills. You should try to make your own additions too.
In this tutorial you saw:
- How to properly handle user input
- How to create and call functions
In the next tutorial we're going to take a short
diversion away from our game and learn about the internals of the
Shiny language. You'll see how the variable typing works, how to
review the Shiny compiler output to check your code and most importantly,
how to access the Brass Virtual Machine debug interface.
|