From Game Editor
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 24x24,
- 320/24=16;
- 240/24=10;
- 16*10=160; //There are 160 cells in this grid.
Cell 0 is the upper left cell, and 159 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;
To make things easier, this example will use a game where view.x is 0 and view.y is 0. To do this, drag your view till the top left corner is over the + that designates game center. This will make coordinates much easier to work with.
First: Define your offset. This is the top left corner of your game grid, which usually is not the top left corner of view. If our game resolution is 640x480, and our board size is 320x240, centered, our offset will be 160, 120. So create two global variables, one called xoffset, and one called yoffset, and set them to these values on startup.
//This function is designed to run from Global Script
int findcell(int xx, int yy) //returns the current cell
{
int vcell=round((xx-xoffset)/24);
int hcell=round((yy-yoffset)/24);
int mycell=vcell+(hcell*16);
return mycell;
}
We've used the line "vcell+(hcell*16)", 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%16)*24)+xoffset;
modulo returns the remainder of division; this shows us our xcoordinate.
int yy=(((mycell-(mycell%16))/16)*24)+yoffset;
We subtract the modulo from the cell number, and then find how many rows (16'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, 12.
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)/24)*24)+12; y=(round((ymouse-yoffset)/24)*24)+12;
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 24 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)/24)*24)+12;
y=(round((ymouse-yoffset)/24)*24)+12;
}
else{
FollowMouse("Event Actor", BOTH_AXIS);
}
Boardwidth and boardheight simply ask for the size of the board, in this case 320 and 240.
If you're wondering why the xoffset and yoffset are used so often, here's why: If you want to change your game, or move the board around any, you'll need to reset these coordinates. That's not good.
So this way, you can set offset to whatever you wish to on startup, so that this script will work for many different board setups.
Note that you can also use cellsize, hcells, etc. variables instead of the magic number '24' 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, they can store the variable 'mycell'.
mycell is simply an integer, actor variable. Since we can determine our cursor cell any time, you can simply send an event to the pieces to tell them 'if mycell==cursor.mycell' and we know which one we've clicked on.
Even better, we can store our variables in cell arrays instead of in the actors, which can save us a lot of time because we don't have to ask actors for their attributes. We can just store them and we'll already know.
For instance, we can create an array to see if cells are empty or full, and then ask the actor to say
cellfull[mycell]=1; cellid[mycell]=cloneindex;
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.
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.
Or we could store the type of piece that's in the cell, like so:
cellcolor[mycell]=mycolor; //in a color matching game (gems).
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.
This loop will look one space in each cardinal direction. We don't use 0, cause that's the cell we're already in!
int matches=0; //this will be used to count matches int temp=0; //this will be used to prevent errors
for(i=1; i<2; i++){
temp=mycell+i; //look one cell to the right
if(temp>=0 && temp<160){ //this prevents us going outside of the array
//i. e. if our cell is 159, we don't want to look at cell 160!
if(cellcolor[i]==mycolor){ //if that cell has the same color piece
matches+=1;} //store that a match was found
temp=mycell+(i*16); //look at the cell below etc. etc. just like above.
temp=mycell-i; //look at the cell to the left etc. etc. } //end loop
Note that we can even grab the cloneindexes of the matches pieces, and store them in a matching array. Since only one clicked cell is triggering this search, we can use a global array of matches, which stores all the cloneindexes of the pieces found matching and in continuous range, and simply wipe that array to 0 before the next search goes to use it.
If we want only continuous (scoring) matches, we can create four local variables (or 8 if you allow diagonals), and whenever we find a piece that isn't the proper color, we can set one of those variables to 1, and only keep looking in directions that still have a value of 0.
So when you click, and we want all the matching pieces to light up, we can use a global timer sent to all the pieces in which they look in the matched array and if their cloneindex number is in that array, they light up. Click again and they'll destroy themselves.
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 click> find current cell, then search for matching cells. Send a timer to the pieces to tell them to light up, but only if any scorable matches were found. Then, when the user clicks on a highlighted cell, we'll send the timer again to destroy the pieces. We only need one timer, and a variable that scoring is in effect or not.
1. Determine current cell 2. Search for matches, and if found, set variable "scorable" to 1; 3. Send the timer to all pieces;
piece>timer 1>
switch(scorable){
case 0: //case 0 means no matches are light up yet
for(i=0; i<160; i++){ //where 160 is the maximum scorable size
if(matches[i]==cloneindex){ //if we're in the scorable group
i=160; //break out of the loop
animpos=1; //switch to lighted animation frame
}
} //end loop
break;
case 1: //if its time to score, we just need to know if we're lit;
if(animpos==1){
DestroyActor("Event Actor");
break;
} //end scoring switch
Note that if you use an animation strip for the different pieces and colors, you'd be basing the action of Animindex rather than animpos, where strip1 is normal colors and strip2 is highlighted colors.
This may seem complicated at first, but once you understand the logic, it will make more sense. What if we want to look 3 spaces in each direction? Or what if we want to look until no more matches are found? This is why we use the loop and the temp variable; we can look as far as we want, and not worry about errors. All you have to do to extend the depth of the search is to increase the size of the loop, and use the results of the loop to tell the pieces what to do.
So while this will find pieces to score in connect 4 or othello or jewel matching, consider how we can use this in a chess game...we can have a rook look down its horizontal and vertical rows to see what moves it has. When we hit a cell that's not empty, we can ask the cell what piece is there and determine if its worth taking. And then the rook can run all the other pieces' move looks in reverse from the target cell;
So if the cell contains a pawn, and rook determines that a bishop is within a bishop's move of that cell, he knows not to take that piece!
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 use PhysicalResponse>top side>piece to stop when they land on a solid piece, and yvelocity+=1 in their draw actor to make them fall in the first place.
Once we initiate a scoring run, and the pieces start destroying themselves, we will disable cursor>mousebuttondown;
We will then create a timer that lasts long enough to let all the pieces fall and land and stop moving; We will also run loops to reset all cell[] arrays to 0 (as applicable).
Then when that timer hits, we'll have the pieces fill in their values again, by finding their cells based on their xy and filling it in with their values. Since timers are global and exact, this all happens in one single frame.
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.
So our board has 16 cells horizontally, so we'll create an array to store this information; (this would be done after cells are finished falling, but before reenabling cursor>mousebuttondown;)
slides[16]; //this will store empty/full column numbers slidex[16]; //this will store the total movement for each column
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;
for(i=0; i<15; i++){ //15 is the far right column, it will never move, so we don't include it.
for(j=i; j<16; j++){ //check for empty columns (all columns){
if(slides[j]==0){ //empty column found
slidex[i]++; //add 1 space to the slide storage variable;
}
}
}
Notice our j loop starts out at j=i; because we don't need to check columns to the right, only to the left. And slidex[i] holds the number of spaces for that column only, because there could be other spaces to the right of it that columns farther right will use, but this one won't.
The result of this loop is that all slidex[i] rows will have a value of 1; So then its just another timer:
piece>timer>
x+=slidex[i]*24; //move 24 pixels for every empty row found
So if column 8 were empty, columns0-7 will get a slidex of 1, while columns 9+ won't get any slidex at all.

![[]](/wiki/skins/blender/open.png)