Creating board/strategy games

From Game Editor

Jump to: navigation, search

Board/Strategy/Puzzle basics

1. The Grid.

The grid is the basis for most strategy oriented games. First create a global array for each value you wish to store. The size of the arrays comes from calculating your board resolution vs. cell size; for instance, if your board is 320x240, and your cell size is 32 x 32,

  • 320/32=10;
  • 240/32=7.5; //we'll dump the .5
  • 10*7=70; //There are 70 cells in this grid.

Cell 0 is the upper left cell, and 69 is the lower right cell.

Now you want to translate and create a function to calculate the cell locations.

Example: Let's determine which cell we're in by using our x and y;

Create an object to handle all the pieces. This actor will be offscreen, and all pieces will be children of this actor. In this way, you can then move this actor at will to move the entire game board at will!

This function is designed to run from Global Script

int findcell(int xx, int yy) //returns the current cell
{
int vcell=round((xx-controller.x)/32);
int hcell=round((yy-controller.y)/32);
int mycell=vcell+(hcell*10); 
return mycell;
}

Usage:

int currentcell=findcell(xmouse, ymouse);

Once you have currrentcell, you can find out what piece the player is clicking on when he clicks on a filled region covering the entire screen.

We've used the line "vcell+(hcell*10)", what we've done here is: Your horizontal (x) cellspace is absolute, it is what it is. But your vertical (y) cellspace includes all the horizontals before it; So every time you move down a cell row, you must count for all 16 spaces in the above row, then add the horizontal location to that.

Now lets take our game cell and determine x and y from that:

int xx=round((mycell%10)*32)+controller.x; 

modulo returns the remainder of division; this shows us our xcoordinate.

int yy=(((mycell-(mycell%10))/10)*32)+controller.y;

We subtract the modulo from the cell number, and then find how many rows (10's) are there, multiply by cellsize and add the offest, and bingo! Y!

Now this will of course snap to the upper left of the cells; to use the center of the cell, simply add 1/2 cellsize to the final product; in this case, 16.


Snapping the Cursor:

So lets say you have a mouse cursor that doesn't move like mouse; instead, we want it to snap to the cells when its on the grid. So create an actor called cursor.

So on cursor>draw actor>

x=(round((xmouse-xoffset)/32)*32)+16;
y=(round((ymouse-yoffset)/32)*32)+16;

Now the cursor snaps to cell centers. What we've done is:

  • 1. subtract the offset
  • 2. divide by the cellsize
  • 3. round that product //to get rid of any coords that aren't snapped
  • 4. multiply by cellsize //inflate back to normal size again
  • 5. add the final cellcenter offset.

Now it only moves in snaps of 32 pixels at a time.

So we may want the cursor to only snap when its over the grid, and move normally when its over the background or menu, etc.

cursor>draw actor>

if(x>xoffset && x<(xoffset+boardwidth) && y>yoffset && y<yoffset+boardheight){ 
FollowMouse("Event Actor", NONE_AXIS);
x=(round((xmouse-xoffset)/32)*32)+16;
y=(round((ymouse-yoffset)/32)*32)+16;
}
else{
FollowMouse("Event Actor", BOTH_AXIS);
}

Boardwidth and boardheight simply ask for the size of the board, in this case 320 and 240.


Note that you can also use cellsize, hcells, etc. variables instead of the magic number '32' i keep using, and then porting your game will be much easier. However, if i used nothing but variable names in these scripts, nobody would be able to make heads or tails of it.

Consider though, how usefull an xoffset and yoffset is; if you have a two player tetris style game, both players can use the same scripts, only with a separate xoffset and yoffset for each player.

2. Eliminating Actor referencing in favor of grid referencing.

Now that we have a grid, and we know exactly where everything is, we don't even need a mousebuttondown on the pieces anymore; because if the pieces use the 'translate xy to cell' function, we always know what piece the mouse is over.

mycell is simply an integer, actor variable. Since we can determine our cursor cell any time, you can simply send grab the piece in question using getclone2 and do whatever you want to it.

To make this work, we have to store the cloneindexes when we create the pieces. We start with a multidimensional array.

In global code:

 int cells[160][3];

So when we create our pieces using a loop, we can assign everything to the appropriate cell:

Controller>CreateActor (or whatever event spawns the pieces)

 int i; int j=0; int k=0;
 int color;
 Actor * this;
 for(i=0; i<70; i++){
 this=CreateActor("jewel", "jewels00", "(none)", "(none)", j*32, k*32, false); 
 color=round(rand(4));
 ChangeAnimationDirection(this->clonename, STOPPED); //stop animation
 cells[i][0]=1;  //empty/not empty
 cells[i][1]=color;  //color of jewel
 cells[i][2]=this->cloneindex;  //store the actor cloneindex
 this->animpos=color;  //assign jewel color to the jewel
 ChangeParent(this->clonename, "controller"); //make jewel a child of controller
 j++; if(j==10){
 j=0; k++;}
 }

Now when we click on a cell, and cursor finds which cell its in, it can simply ask if there's anything in that cell or not. And now we can even retrieve the cloneindex of the piece from the id array.

We can also highlight the piece the mouse is over in the same way, by using the getcellfromxy function, then grabbing the piece using actor * and the cloneindex slot in the array.


By the same token, in land-based strategy games, we can use an array to define land types;

celltype[mycell]=1;

where 0 is dirt, 1 is grass, 2 is water, 3 is ice, 4 is mountain.

We can even then use those numbers to calculate land bonuses; for instance, we can subtract celltype[mycell]*.1 from our speed; so mountains will make us move slower than dirt.

Finding Matching Jewels

Then when you click on a piece, to see if there are any matching pieces nearby, we don't have to care about the actors themselves, if the actors fill in the array with their colors, we can just search the array.

What we generally do is to loop through the entire game board, and look for matches.

int matches=0;   //this will be used to count matches
int temp=0;      //this will be used to prevent errors
int tcolor=0; //temp color 
int prevtcolor=999;// the previous temp color - start with unusable value
int i; int j; int k;
for (j=0; j<7; j++){  //for each column
for (k=0; k<10; k++){ //move down the row
i=j*10+k;   //10 for each row....
if(cells[i][1] != prevtcolor || k==0){ //if our cell doesn't match the previous color, or we went on to a new row
tcolor=cells[i][1];  //this color is our new color
prevcolor=tcolor;
temp=0;}
else{   //if the cell is the same color as we had before
temp++; //increase the number of same color pieces found
}
if(temp==3){ //match 3 and we have a score!
matches ++;
}
}

Now of course, we need to check for vertical matches as well; We'll use a similar loop, only searching verically....

for(j=0; j<10; j++){ //for each column
for(k=0; k<7; k++){
i=j+(k*10).........//everything runs the same as the previous search.

Then at any time, you can grab all the cloneindexes of the pieces in the cells that match, and destroy them.


Then, when you let the pieces fall to replace destroyed pieces, we'll wipe all the arrays and let the pieces fill them in BEFORE we enable the user to make another mouseclick, so we really don't need any DestroyActor scripts (at least not for gameplay).

So the whole event system works like this:

1. Determine current cell 2. Search for matches; 3. Grab and destroy all pieces that were scorable 4. Determine the number of blank cells in each column now 5. Tell the cells above each blank to moveto() the proper location; 6. Spawn the necessary new jewels and have the moveto() as well 7. Wait for all jewels to finish their movetos and then start the loop again.

We can do some neat things by writing these as separate functions. For instance, we can fun step 2 but not step 3, and use that to determine if player is about to make a valid move or not, then run step 3 only if he has just made a valid move.

We can increase a count every time a moveto() command is issued, and decrease that count every time a moveto is finished - so we can always know if the pieces are all done moving or not.

3. Falling, disappearing, and row movement.

When we destroy a piece, we don't need to know anything about it anymore, it doesn't have to set cellfull[mycell] to 0 or anything. When pieces are destroyed, the other pieces will recieve moveto() commands to tell them where to move next.

Once we initiate a scoring run, and the pieces start destroying themselves, we will disable cursor>mousebuttondown;

We count up a variable when a moveto is issued, and decrease it when a moveto is finished.

Then when all pieces are finished, we wipe the arrays and start again.

So on the next frame, our variables are all set, and we're ready to reenable the mousebuttondown even for cursor.


So what happens when a column is empty, and we want all the pieces to slide over to fill that space? We're not using velocity or physical response this time; It would be bad if pieces could bounce sideways.

Instead, we're going to use another loop to count spaces and move accordingly.

And when the pieces fill in the array slots, they'll fill in slides too; piece>timer>

int tempx=(x-xoffset)/24; //(this gives us the vertical column they're in).
slides[tempx]=1;     

Remember that we wipe all arrays after using them, so if there are no pieces in a column, it won't ever get set to 1.

So lets say column 15 (the far right column) is blank, so all the pieces on the whole board need to shift to the right.

First, we start the column loop, where i is the column to move, and j is the column to check.

int i;
int j;         
int k;
int m;
int found;
for(j=9; j>=0; j--){ //we start from the far right
found=0; //reset blanks found
for(k=6; k>=0; k--){ //we start counting from the bottom...
i=j+(k*10); 
if(cells[i][0]==0){ //if a cell is marked as empty){
found++;}
else if(cells[i][0]==1){ //else if cell has a jewel
this=getclone2("jewel", cells[i][2]);
MoveTo(this->clonename, this->x, this->y+(found*32), fallspeed, "", "", Game Center);
//piece moves down 32 pixels for every space found!
allmoved++; //increment our 'pieces in current motion' variable.
} //finished finding and moving pieces
if(k==0){ //on the last piece counting upward
for(m=0; m<found; m++){  //for each hole, spawn a new piece!
this=CreateActor("jewel", "jewel00", etc etc.);
color=rand(4);
this->animpos=color;
MoveTo(this->clonename, j*32, found*32, etc. etc.)
allmoved++;
found--; //now we decrease found for each new piece we've made!!!!
//we assign only what we need to create the piece; we will set array values once it's finished it's moveTO();
}//end m loop
}//end k==0
}//end k loop
}//end i loop

And on jewel>move Finish

int mycell=findcell(x, y);
cells[mycell][0]=1; //not empty...
cells[mycell][1]=animpos;
cells[mycell][2]=cloneindex;
allmoved--;


Now we'll just wait for our allmoved variable to be 0 again.