My humblest apologies for just disappearing like I have. I really did not mean to do that especially after I promised that I would keep this thread alive. Thanks to AA for doing that in my extended absence. It is funny how life sometimes pushes you in a direction that you do not really want to go. Things have been really hectic this end of the world and if you've kept your eye on the news you'll know what I mean.
Enough of all that. Lets hope this time I am here to stay. It looks like I really do have a lot of reading to do so I will not post while catching up.
Again I want to make it clear that I am deeply sorry for just vanishing like that.
No problem wokani. The next update will have something that compiles and runs which will be enough to get the tweaking and polishing started. But man, it's going to look wrong at the beginning :-D
It's well time to get this thing into a stride again. I spent some time on the weekend intending to add the last bunch of code I have only to discover some of it is wrong and some of it has to be redone. So the next update will be a little bit delayed.
In the meantime, in order to keep things alive and familiarize potential contributors with how writing to the display and movement/animation of sprites is done, I thought I'd post snippets of what I am working on. The graphics, animation, text and general polishing will be the major tasks required of contributors. Aside from the scary AI problem.
The first question to tackle is how to deal with sprite animation. If you look at some of the sprites in the last mule update, you'll see they're all 3 chars tall and 2 chars wide. Some have 8 frames of animation, some 7, some 4, and probably others. Additionally, this is not a complete set of sprites required by the game. Some small 1 char x 1 char sprites will be required and they may have 2 or 3 frames of animation as well. To help prevent the code from exploding to handle all these separate specialized cases, the idea is to have a single function able to return the next sprite frame to use for all flavours of sprites, given current animation frame and a command (continue, left, right, etc). Sounds complicated, but it really isn't when a state machine is brought into the picture.
Since SP1 will be handling the sprites we have to marry this idea with how SP1 performs sprite animation. SP1 generates a sprite graphic address by adding two values:
1. The graphic address specified when the sprite is created through calls to "sp1_CreateSpr()" and "sp1_AddColSpr()". You can think of this as a fixed base address for a specific sprite.
2. The animation offset specified during a "sp1_MoveSpr*()" call or written directly into the "struct sp1_ss.offset" member.
If your sprite is 3 chars tall x 2 chars wide and does not have a mask, then each sprite frame graphic will require 3*2*8 = 48 bytes of memory. If this sprite has 8 frames of animation and those frames are stored in order one after the other beginning at address "base", then we can do the following.
When the sprite is created using "sp1_CreateSpr()" and "sp1_AddColSpr()" we use the "base" address as graphic pointer. In the "sp1_MoveSpr*()" calls we select which animation frame to use by specifying an offset of 48*0, 48*1, 48*2, ..., 48*7 depending on which frame 0-7 we want. The sprite engine will use the address "base + 48*f" as the address of the graphic to use when drawing the sprite. In this scheme, "base" is the address specified when creating the sprite and "f' is the frame number 0-7.
So the idea is to have a single function that takes two parameters: a state that indicates the current sprite frame and what type of sprite and a command that indicates what we want to do with the sprite.
Here is the prototype for this function:
uint gfx_GetSpriteFrame(uchar *state, uchar cmd);
We pass a pointer to the state of the sprite because this function will change the sprite state depending on what the command is. There are several commands that will make writing sprite animations in the game easy:
FRAME_NOCHANGE
the sprite frame used is not changed (sprite image stays the same)
FRAME_NEXT
select the next sprite frame in the animation sequence; if the player is moving left, eg, the next left frame is selected
FRAME_LEFT
change sprite direction to the left
FRAME_RIGHT
change sprite direction to the right
FRAME_UP
change sprite direction to up
FRAME_DOWN
change sprite direction to down
FRAME_RESET
a flag that is meant to be ORed with one of the directions (FRAME_LEFT, etc) and causes selection of the first frame in the corresponding animation sequence; this is intended as an initialization command when used in combination with one of the directions
The function returns an offset value to use with the "sp1_MoveSpr*()" functions that corresponds to the current animation frame to use.
The "state" value passed to the function encodes such things as current animation frame, sprite size and sprite direction. This way the same function can be used for all sprites no matter what the sprite size or the number of animation frames involved. The function has not been written yet as we must have all sprite graphics to write it but it is fairly straightforwardly implemented as a state machine. If this is a new idea to you, hold tight. For now we can use this function when animating sprites. An example of its usage is coming up below.
The next problem I came across is that the "d_Print()" function we have is cumbersome to use. "d_Print()" was described in this thread earlier and is the function that prints either normal-width or double-width text to the screen. It understands several basic formatting commands such as move the cursor to a specific (x,y) location, change colour, right-justify, etc. The cumbersome part comes in because these commands can only appear at the beginning of a new line; they cannot appear in the middle of some text.
So, eg, if I wanted to print "score" at (0,0), move the cursor to (0,10) and add the score value, I could not do this:
The extra '\n' forces a new line so that d_Print() will interpret the following '\x16' as a move cursor command rather than a character to print. This little aggravation causes text strings to be a lot longer, taking up more memory, and quite frankly annoys the hell out of me. So you'll be seeing a new d_Print() here shortly that has been modified to allow these commands to be present anywhere in the string, not just following a '\n'.
All this setup for the juicy bit in the next post...
The status summary is a scoring summary. The four players, in order of highest score to lowest score, march from the right to the left at the bottom of the screen, then upwards when they reach column 1. When they're in their final position, the scores are printed to screen.
Here is a couple of images from the original Atari version to help with how things are supposed to look:
// STATUS SUMMARY
// players assumed to be sorted in score order
static int ss_initpos(struct player *p)
{
// select first left-facing sprite frame and place sprite initially on row 21
sp1_MoveSprAbs(p->sprite, &gfx_clip, gfx_GetSpriteFrame(&p->state, FRAME_RESET | FRAME_LEFT), 21, g_uchar0, 0, 0);
// following sprite placed three characters to the right
g_char0 += 3;
return 0;
}
static int ss_moveparade(struct player *p)
{
// movement of two pixels at a time ensures the horizontal march takes about 5s, matching original Atari version
char dx=0;
char dy=0;
uint offset=0;
// stop visiting players if the movement timer hasn't gone off
if (g_uchar0) return 1;
// if it's time to change animation frame
if (!g_uchar1) offset = gfx_GetSpriteFrame(&p->state, FRAME_NEXT);
// which way is the sprite moving?
if ((p->sprite->col == 1) && (!p->sprite->hrot))
{
// vertical movement
dy = -2;
if (p->sprite->row == 21)
{
// sprite changes from horizontal movement to vertical movement
offset = gfx_GetSpriteFrame(&p->state, FRAME_UP);
}
else if ((p->sprite->row == (g_uchar2*4 + 3)) && (!p->sprite->vrot))
{
// sprite has reached its final resting place
offset = gfx_GetSpriteFrame(&p->state, FRAME_DOWN | FRAME_RESET);
dy = 0;
}
}
else
{
// sprite moving horizontally
dx = -2;
}
sp1_MoveSprRel(p->sprite, &gfx_clip, offset, 0, 0, dy, dx);
++g_uchar2;
return 0;
}
static int ss_printscore(struct player *p)
{
uchar clr;
clr = gfx_playerColour[p - g_players] & 0x07; // get player colour in ink with black paper
d_Print(g_uchar0, 4, clr, 2, "MONEY"); // double width left justified
ltoa(g_textBuffer, pyr_MoneyScore(p));
d_Print(g_uchar0++, 31, clr, 6, g_textBuffer); // double width right justified
d_Print(g_uchar0, 4, clr, 2, "LAND");
ltoa(g_textBuffer, pyr_LandScore(p));
d_Print(g_uchar0++, 31, clr, 6, g_textBuffer);
d_Print(g_uchar0, 4, clr, 2, "GOODS");
ltoa(g_textBuffer, pyr_GoodsScore(p));
d_Print(g_uchar0++, 31, clr, 6, g_textBuffer);
d_Print(g_uchar0, 4, clr, 2, "TOTAL");
ltoa(g_textBuffer, p->score);
d_Print(g_uchar0++, 31, clr, 6, g_textBuffer);
return 0;
}
void Status_Summary(void)
{
long colonyScore;
colonyScore = g_players[0].score + g_players[1].score + g_players[2].score + g_players[3].score;
// clear screen
sp1_ClearRectInv(&gfx_clip, INK_WHITE | PAPER_BLACK, ' ', SP1_RFLAG_TILE | SP1_RFLAG_COLOUR);
zx_border(BLACK);
sp1_UpdateNow();
// move player sprites to their initial positions for the parade
g_uchar0 = 30; // column positon of first sprite
pyr_VisitAllPlayersSorted(ss_initpos);
sp1_UpdateNow();
// player parade
g_downtimer0 = 2; // move sprites horizontally every 2 frames
g_uchar1 = 12; // 500ms animation rate (12*2 interrupts)
do
{
// move all sprites
g_uchar0 = g_downtimer0; // take a snapshot of the timer
if (!g_uchar0) --g_uchar1; // update animation rate counter
g_uchar2 = 0; // keeps track of player placement, begins at first place
pyr_VisitAllPlayersSorted(ss_moveparade);
sp1_UpdateNow();
// reset timers
if (!g_uchar0) g_downtimer0 = 2;
if (!g_uchar1) g_uchar1 = 12;
// keep going until first place sprite is in final position
} while ((g_sortedPlayers[0]->sprite->row != 3) && (g_sortedPlayers[0]->sprite->vrot));
// print score text
strcpy(g_textBuffer, "STATUS SUMMARY \x01");
u_AppendInt(g_textBuffer, g_round);
strcat(g_textBuffer, "x01\x16\x13\x02COLONY\x16\x13\x1f\x04");
u_AppendLong(colonyScore);
d_Print(0, 0, INK_YELLOW | PAPER_BLACK, 2, g_textBuffer);
g_uchar0 = 2; // first place player stats start in row 2
g_VisitAllPlayersSorted(ss_printscore);
// colony messages
if (g_round == ((g_level == LEVEL_BEGINNER) ? 6 : 12))
{
// last round
strcpy(g_textBuffer, "OVERALL, THE COLONY ");
switch ((int)(colonyScore / 20000L))
{
case 0:
strcat(g_textBuffer, "FAILED...DISMALLY. THE FEDERATION DEBTOR'S PRISON IS YOUR NEXT HOME!");
break;
case 1:
strcat(g_textBuffer, "FAILED... THE FEDERATION WILL NO LONGER SEND TRADE SHIPS. YOU ARE ON YOUR OWN!");
break;
case 2:
strcat(g_textBuffer, "SURVIVED...BARELY. YOU WILL BE LIVING IN TENTS. FEW TRADING SHIPS WILL COME YOUR WAY!");
break;
case 3:
strcat(g_textBuffer, "WAS A SUCCESS. YOU HAVE MET THE MINIMUM STANDARDS SET BY THE FEDERATION BUT YOUR LIFE WILL NOT BE EASY!");
break;
case 4:
strcat(g_textBuffer, "SUCCEEDED. THE FEDERATION IS PLEASED BY YOUR EFFORTS. YOU WILL LIVE COMFORTABLY!");
break;
case 5:
strcat(g_textBuffer, "SUCCEEDED...EXTREMELY WELL. YOU CAN NOW RETIRE IN ELEGANT ESTATES!");
break;
default:
strcat(g_textBuffer, "DELIGHTED THE FEDERATION WITH YOUR EXCEPTIONAL ACHIEVEMENT. YOUR RETIREMENT WILL BE LUXURIOUS!");
break;
}
strcat(g_textBuffer, " "); // a few spaces in case text wraps in scroll
d_ScrollStart();
}
// else food, energy, smithore shortages
else
{
// no message
d_Print(22, 0, INK_WHITE | PAPER_BLACK, 0, "PRESS ALL PLAYER BUTTONS TO CONT");
}
}
The function "Status_Summary()" is called assuming player scores have already been calculated (and stored in "struct player.score" -- see player.h) and players have already been sorted into 1st, 2nd, etc in the "g_sortedPlayers[]" array (see player.h and pyr_SortPlayersByScore() in player.c).
The colony score is computed and the screen is cleared to white ink on black paper.
The next bit of code:
// move player sprites to their initial positions for the parade
g_uchar0 = 30; // column positon of first sprite
pyr_VisitAllPlayersSorted(ss_initpos);
sp1_UpdateNow();
positions the sprites for the march. The initial position of the sprites is at the right edge near the bottom of the screen. The first place player is initially visible on screen at column 30 but other players appear to his right, off screen and separated by 3 characters. It may have been a while but "pyr_VisitAllPlayersSorted()" is a function that should be familiar to you. It visits all players in order from 1st place to last, calling the helper function "ss_initpos(struct player *p)" with a pointer to the player currently being visited. If you need a refresher you can find it in player.c. The global variable "g_uchar0" is used to keep track of which column the next sprite should be placed in. You'll see in "ss_initpos()" that after g_uchar0 is used to move the sprite to its initial location, "g_uchar0" is increased by 3 for the next player. Also in "ss_initpos()" you'll see the use of the new "gfx_GetSpriteFrame()" to select the first left-facing frame of the sprite.
Once the sprites are initially placed (only the first place player is visible, the others are still off-screen), the march begins. This march has the sprites walk left until they reach the first column when they will march upward to their final placement. I am using a timer and a counter to keep the sprite movement and animation rate constant and consistent. "g_downtimer0" counts down to 0 and is updated during the 50Hz interrupt (see interrupt.c). It will be used to determine when the sprites need to move. "g_uchar1" is a counter used to determine when a new sprite frame needs to be selected. In the TG image editor the sprite frames are animated at a 500ms rate. Since "g_uchar1" gets decremented every time "g_downtimer0" reaches 0, this means its value of 12 will reach 0 after 12*2=24 interrupts = 24/50s = 480ms which matches the TG image editor closely.
With the timer and counter intiialized, the "do-while" loop is entered, which manages the player march. First we take a snapshot of the value of "g_downtimer0" in "g_uchar0". We must do this because the value of "g_downtimer0" can change at any point in the code since it is modified by the interrupt routine and we need a consistent value during each iteration of the do-while loop for things to work correctly.
Again we visit all players in sorted order (first to last place) and use the helper funciton "ss_moveparade()" to move each player. Once the players have been moved and the screen drawn, the timers are updated if necessary. The first place player will be the last to reach its final position. So the condition for terminating the parade loop in the "while" part of the "do-while" is to check if the first place sprite is in its final place. This is done by checking if the first place sprite has reached row 3 without any downward vertical pixel shift.
The function "ss_moveparade(struct player *p)" implements the smarts of the player sprite movement. It must initially walk sprites leftward along the bottom row until column 3 is reached when the sprite must be walked upward to its final position. The player sprites reach their final position at different times so the function must also take care not to move the sprite when it has got to its final position. You can trace through the if..else code to see how this is all accomplished and take special note of how "gfx_GetSpriteFrame()" is used to select the correct animation frame. Don't forget a 0 value used as offset in "sp1_MoveSpr*()" functions indicates to SP1 that the same sprite frame as last time should be reused.
One last comment on "ss_moveparade()": the Atari version has the sprites complete the horizontal part of the walk in about 5 seconds. For us, that's a movement from column 30 to column 1 (232 pixels). Since the subroutine is set up to move the sprite every 2 interrupts (g_downtimer0=2), this means the player sprites should be moved horizontally by 232/5*2/50 = 1.856 pixels every 2 interrupts to achieve a 5 second walk. A movement of 2 pixels was chosen to approximate this.
With the parade done, the scores are printed. The code should be easy to follow by now. The strings containing the scores contain some formatting information passed to d_Print() and hence the buried "\xHH" in the strings. See d_print() in display.c for a review of what codes are implemented.
And finally, messages may scroll across the bottom of the screen depending on whether the last round has been reached or if there are colony shortages (with the latter messages not added yet). The calling function must wait for all players to press their fire buttons before continuing.
Hey all. I've been absent for ages due to varying circumstances (relationship, moving, kids, etc. etc.) and AGAIN i'll be moving soon (on the 13th of Feb to be precise), but I thought i'd pop back on and find out the progress of M.U.L.E and see if I could be of any more help when I return on a more permanent basis. Hopefully the stuff i've done has proved useful, but if not, I still have all the relevant files and can make changes or start afresh if need be once the 'net gets sorted at my new place.
Happy moving day! :-) The project is still open but hasn't moved an inch since I've been so busy the last few months that WOS and ZX has been misplaced in my daily schedule.
I'm going to move this project to CVS at z88dk.org when I get a chance and get it into a (at least) partial demo state so that you can see the graphics and make tweaks to it easily.
I've just ordered a netbook so I'm thinking maybe that will create some time to work on such things during the daily commute.
I lost all the most recent code of all my zx related projects a month or so ago when my sd drive died. It was missing AI (no docs existed at that time on how the AI worked in the original) and I had not actually run the program as a whole using the graphics supplied because there was a memory problem.
The memory problem was being solved by adding / reimplementing library routines in z88dk and writing a simple and small sprite routine. sp1 does a lot of things but when a game only needs something simple, its memory footprint can get in the way. With these changes I am pretty sure the result would fit into 48k, depending on how nasty the AI got (it may or may not require a good chunk of RAM to search a decision space).
Winston also made a lot of progress on spectranet so part of the library update in z88dk was a rewrite of stdio so spectranet could seemlessly and (hopefully) efficiently be plugged into that. Mule is a great candidate for a local area network game :-)
I still have great interest in finishing this and I believe I still have a good amount of code on another computer since this project was started so long ago but it is always available time that interferes. I haven't managed to complete much in the past 2 years. I should probably reconsider my priorities :D
Comments
My humblest apologies for just disappearing like I have. I really did not mean to do that especially after I promised that I would keep this thread alive. Thanks to AA for doing that in my extended absence. It is funny how life sometimes pushes you in a direction that you do not really want to go. Things have been really hectic this end of the world and if you've kept your eye on the news you'll know what I mean.
Enough of all that. Lets hope this time I am here to stay. It looks like I really do have a lot of reading to do so I will not post while catching up.
Again I want to make it clear that I am deeply sorry for just vanishing like that.
Kind regards,
Adriaan
Write games in C using Z88DK and SP1
It's well time to get this thing into a stride again. I spent some time on the weekend intending to add the last bunch of code I have only to discover some of it is wrong and some of it has to be redone. So the next update will be a little bit delayed.
In the meantime, in order to keep things alive and familiarize potential contributors with how writing to the display and movement/animation of sprites is done, I thought I'd post snippets of what I am working on. The graphics, animation, text and general polishing will be the major tasks required of contributors. Aside from the scary AI problem.
The first question to tackle is how to deal with sprite animation. If you look at some of the sprites in the last mule update, you'll see they're all 3 chars tall and 2 chars wide. Some have 8 frames of animation, some 7, some 4, and probably others. Additionally, this is not a complete set of sprites required by the game. Some small 1 char x 1 char sprites will be required and they may have 2 or 3 frames of animation as well. To help prevent the code from exploding to handle all these separate specialized cases, the idea is to have a single function able to return the next sprite frame to use for all flavours of sprites, given current animation frame and a command (continue, left, right, etc). Sounds complicated, but it really isn't when a state machine is brought into the picture.
Since SP1 will be handling the sprites we have to marry this idea with how SP1 performs sprite animation. SP1 generates a sprite graphic address by adding two values:
1. The graphic address specified when the sprite is created through calls to "sp1_CreateSpr()" and "sp1_AddColSpr()". You can think of this as a fixed base address for a specific sprite.
2. The animation offset specified during a "sp1_MoveSpr*()" call or written directly into the "struct sp1_ss.offset" member.
If your sprite is 3 chars tall x 2 chars wide and does not have a mask, then each sprite frame graphic will require 3*2*8 = 48 bytes of memory. If this sprite has 8 frames of animation and those frames are stored in order one after the other beginning at address "base", then we can do the following.
When the sprite is created using "sp1_CreateSpr()" and "sp1_AddColSpr()" we use the "base" address as graphic pointer. In the "sp1_MoveSpr*()" calls we select which animation frame to use by specifying an offset of 48*0, 48*1, 48*2, ..., 48*7 depending on which frame 0-7 we want. The sprite engine will use the address "base + 48*f" as the address of the graphic to use when drawing the sprite. In this scheme, "base" is the address specified when creating the sprite and "f' is the frame number 0-7.
So the idea is to have a single function that takes two parameters: a state that indicates the current sprite frame and what type of sprite and a command that indicates what we want to do with the sprite.
Here is the prototype for this function:
We pass a pointer to the state of the sprite because this function will change the sprite state depending on what the command is. There are several commands that will make writing sprite animations in the game easy:
FRAME_NOCHANGE
the sprite frame used is not changed (sprite image stays the same)
FRAME_NEXT
select the next sprite frame in the animation sequence; if the player is moving left, eg, the next left frame is selected
FRAME_LEFT
change sprite direction to the left
FRAME_RIGHT
change sprite direction to the right
FRAME_UP
change sprite direction to up
FRAME_DOWN
change sprite direction to down
FRAME_RESET
a flag that is meant to be ORed with one of the directions (FRAME_LEFT, etc) and causes selection of the first frame in the corresponding animation sequence; this is intended as an initialization command when used in combination with one of the directions
The function returns an offset value to use with the "sp1_MoveSpr*()" functions that corresponds to the current animation frame to use.
The "state" value passed to the function encodes such things as current animation frame, sprite size and sprite direction. This way the same function can be used for all sprites no matter what the sprite size or the number of animation frames involved. The function has not been written yet as we must have all sprite graphics to write it but it is fairly straightforwardly implemented as a state machine. If this is a new idea to you, hold tight. For now we can use this function when animating sprites. An example of its usage is coming up below.
The next problem I came across is that the "d_Print()" function we have is cumbersome to use. "d_Print()" was described in this thread earlier and is the function that prints either normal-width or double-width text to the screen. It understands several basic formatting commands such as move the cursor to a specific (x,y) location, change colour, right-justify, etc. The cumbersome part comes in because these commands can only appear at the beginning of a new line; they cannot appear in the middle of some text.
So, eg, if I wanted to print "score" at (0,0), move the cursor to (0,10) and add the score value, I could not do this:
I would be forced to do this:
The extra '\n' forces a new line so that d_Print() will interpret the following '\x16' as a move cursor command rather than a character to print. This little aggravation causes text strings to be a lot longer, taking up more memory, and quite frankly annoys the hell out of me. So you'll be seeing a new d_Print() here shortly that has been modified to allow these commands to be present anywhere in the string, not just following a '\n'.
All this setup for the juicy bit in the next post...
Write games in C using Z88DK and SP1
The status summary is a scoring summary. The four players, in order of highest score to lowest score, march from the right to the left at the bottom of the screen, then upwards when they reach column 1. When they're in their final position, the scores are printed to screen.
Here is a couple of images from the original Atari version to help with how things are supposed to look:
The march
http://www.mediafire.com/download.php?tehrmdpqxdn
The summary
http://www.mediafire.com/download.php?0ms5mxvj2v1
And here is the code that implements it:
// STATUS SUMMARY // players assumed to be sorted in score order static int ss_initpos(struct player *p) { // select first left-facing sprite frame and place sprite initially on row 21 sp1_MoveSprAbs(p->sprite, &gfx_clip, gfx_GetSpriteFrame(&p->state, FRAME_RESET | FRAME_LEFT), 21, g_uchar0, 0, 0); // following sprite placed three characters to the right g_char0 += 3; return 0; } static int ss_moveparade(struct player *p) { // movement of two pixels at a time ensures the horizontal march takes about 5s, matching original Atari version char dx=0; char dy=0; uint offset=0; // stop visiting players if the movement timer hasn't gone off if (g_uchar0) return 1; // if it's time to change animation frame if (!g_uchar1) offset = gfx_GetSpriteFrame(&p->state, FRAME_NEXT); // which way is the sprite moving? if ((p->sprite->col == 1) && (!p->sprite->hrot)) { // vertical movement dy = -2; if (p->sprite->row == 21) { // sprite changes from horizontal movement to vertical movement offset = gfx_GetSpriteFrame(&p->state, FRAME_UP); } else if ((p->sprite->row == (g_uchar2*4 + 3)) && (!p->sprite->vrot)) { // sprite has reached its final resting place offset = gfx_GetSpriteFrame(&p->state, FRAME_DOWN | FRAME_RESET); dy = 0; } } else { // sprite moving horizontally dx = -2; } sp1_MoveSprRel(p->sprite, &gfx_clip, offset, 0, 0, dy, dx); ++g_uchar2; return 0; } static int ss_printscore(struct player *p) { uchar clr; clr = gfx_playerColour[p - g_players] & 0x07; // get player colour in ink with black paper d_Print(g_uchar0, 4, clr, 2, "MONEY"); // double width left justified ltoa(g_textBuffer, pyr_MoneyScore(p)); d_Print(g_uchar0++, 31, clr, 6, g_textBuffer); // double width right justified d_Print(g_uchar0, 4, clr, 2, "LAND"); ltoa(g_textBuffer, pyr_LandScore(p)); d_Print(g_uchar0++, 31, clr, 6, g_textBuffer); d_Print(g_uchar0, 4, clr, 2, "GOODS"); ltoa(g_textBuffer, pyr_GoodsScore(p)); d_Print(g_uchar0++, 31, clr, 6, g_textBuffer); d_Print(g_uchar0, 4, clr, 2, "TOTAL"); ltoa(g_textBuffer, p->score); d_Print(g_uchar0++, 31, clr, 6, g_textBuffer); return 0; } void Status_Summary(void) { long colonyScore; colonyScore = g_players[0].score + g_players[1].score + g_players[2].score + g_players[3].score; // clear screen sp1_ClearRectInv(&gfx_clip, INK_WHITE | PAPER_BLACK, ' ', SP1_RFLAG_TILE | SP1_RFLAG_COLOUR); zx_border(BLACK); sp1_UpdateNow(); // move player sprites to their initial positions for the parade g_uchar0 = 30; // column positon of first sprite pyr_VisitAllPlayersSorted(ss_initpos); sp1_UpdateNow(); // player parade g_downtimer0 = 2; // move sprites horizontally every 2 frames g_uchar1 = 12; // 500ms animation rate (12*2 interrupts) do { // move all sprites g_uchar0 = g_downtimer0; // take a snapshot of the timer if (!g_uchar0) --g_uchar1; // update animation rate counter g_uchar2 = 0; // keeps track of player placement, begins at first place pyr_VisitAllPlayersSorted(ss_moveparade); sp1_UpdateNow(); // reset timers if (!g_uchar0) g_downtimer0 = 2; if (!g_uchar1) g_uchar1 = 12; // keep going until first place sprite is in final position } while ((g_sortedPlayers[0]->sprite->row != 3) && (g_sortedPlayers[0]->sprite->vrot)); // print score text strcpy(g_textBuffer, "STATUS SUMMARY \x01"); u_AppendInt(g_textBuffer, g_round); strcat(g_textBuffer, "x01\x16\x13\x02COLONY\x16\x13\x1f\x04"); u_AppendLong(colonyScore); d_Print(0, 0, INK_YELLOW | PAPER_BLACK, 2, g_textBuffer); g_uchar0 = 2; // first place player stats start in row 2 g_VisitAllPlayersSorted(ss_printscore); // colony messages if (g_round == ((g_level == LEVEL_BEGINNER) ? 6 : 12)) { // last round strcpy(g_textBuffer, "OVERALL, THE COLONY "); switch ((int)(colonyScore / 20000L)) { case 0: strcat(g_textBuffer, "FAILED...DISMALLY. THE FEDERATION DEBTOR'S PRISON IS YOUR NEXT HOME!"); break; case 1: strcat(g_textBuffer, "FAILED... THE FEDERATION WILL NO LONGER SEND TRADE SHIPS. YOU ARE ON YOUR OWN!"); break; case 2: strcat(g_textBuffer, "SURVIVED...BARELY. YOU WILL BE LIVING IN TENTS. FEW TRADING SHIPS WILL COME YOUR WAY!"); break; case 3: strcat(g_textBuffer, "WAS A SUCCESS. YOU HAVE MET THE MINIMUM STANDARDS SET BY THE FEDERATION BUT YOUR LIFE WILL NOT BE EASY!"); break; case 4: strcat(g_textBuffer, "SUCCEEDED. THE FEDERATION IS PLEASED BY YOUR EFFORTS. YOU WILL LIVE COMFORTABLY!"); break; case 5: strcat(g_textBuffer, "SUCCEEDED...EXTREMELY WELL. YOU CAN NOW RETIRE IN ELEGANT ESTATES!"); break; default: strcat(g_textBuffer, "DELIGHTED THE FEDERATION WITH YOUR EXCEPTIONAL ACHIEVEMENT. YOUR RETIREMENT WILL BE LUXURIOUS!"); break; } strcat(g_textBuffer, " "); // a few spaces in case text wraps in scroll d_ScrollStart(); } // else food, energy, smithore shortages else { // no message d_Print(22, 0, INK_WHITE | PAPER_BLACK, 0, "PRESS ALL PLAYER BUTTONS TO CONT"); } }The function "Status_Summary()" is called assuming player scores have already been calculated (and stored in "struct player.score" -- see player.h) and players have already been sorted into 1st, 2nd, etc in the "g_sortedPlayers[]" array (see player.h and pyr_SortPlayersByScore() in player.c).
The colony score is computed and the screen is cleared to white ink on black paper.
The next bit of code:
positions the sprites for the march. The initial position of the sprites is at the right edge near the bottom of the screen. The first place player is initially visible on screen at column 30 but other players appear to his right, off screen and separated by 3 characters. It may have been a while but "pyr_VisitAllPlayersSorted()" is a function that should be familiar to you. It visits all players in order from 1st place to last, calling the helper function "ss_initpos(struct player *p)" with a pointer to the player currently being visited. If you need a refresher you can find it in player.c. The global variable "g_uchar0" is used to keep track of which column the next sprite should be placed in. You'll see in "ss_initpos()" that after g_uchar0 is used to move the sprite to its initial location, "g_uchar0" is increased by 3 for the next player. Also in "ss_initpos()" you'll see the use of the new "gfx_GetSpriteFrame()" to select the first left-facing frame of the sprite.
Once the sprites are initially placed (only the first place player is visible, the others are still off-screen), the march begins. This march has the sprites walk left until they reach the first column when they will march upward to their final placement. I am using a timer and a counter to keep the sprite movement and animation rate constant and consistent. "g_downtimer0" counts down to 0 and is updated during the 50Hz interrupt (see interrupt.c). It will be used to determine when the sprites need to move. "g_uchar1" is a counter used to determine when a new sprite frame needs to be selected. In the TG image editor the sprite frames are animated at a 500ms rate. Since "g_uchar1" gets decremented every time "g_downtimer0" reaches 0, this means its value of 12 will reach 0 after 12*2=24 interrupts = 24/50s = 480ms which matches the TG image editor closely.
With the timer and counter intiialized, the "do-while" loop is entered, which manages the player march. First we take a snapshot of the value of "g_downtimer0" in "g_uchar0". We must do this because the value of "g_downtimer0" can change at any point in the code since it is modified by the interrupt routine and we need a consistent value during each iteration of the do-while loop for things to work correctly.
Again we visit all players in sorted order (first to last place) and use the helper funciton "ss_moveparade()" to move each player. Once the players have been moved and the screen drawn, the timers are updated if necessary. The first place player will be the last to reach its final position. So the condition for terminating the parade loop in the "while" part of the "do-while" is to check if the first place sprite is in its final place. This is done by checking if the first place sprite has reached row 3 without any downward vertical pixel shift.
The function "ss_moveparade(struct player *p)" implements the smarts of the player sprite movement. It must initially walk sprites leftward along the bottom row until column 3 is reached when the sprite must be walked upward to its final position. The player sprites reach their final position at different times so the function must also take care not to move the sprite when it has got to its final position. You can trace through the if..else code to see how this is all accomplished and take special note of how "gfx_GetSpriteFrame()" is used to select the correct animation frame. Don't forget a 0 value used as offset in "sp1_MoveSpr*()" functions indicates to SP1 that the same sprite frame as last time should be reused.
One last comment on "ss_moveparade()": the Atari version has the sprites complete the horizontal part of the walk in about 5 seconds. For us, that's a movement from column 30 to column 1 (232 pixels). Since the subroutine is set up to move the sprite every 2 interrupts (g_downtimer0=2), this means the player sprites should be moved horizontally by 232/5*2/50 = 1.856 pixels every 2 interrupts to achieve a 5 second walk. A movement of 2 pixels was chosen to approximate this.
With the parade done, the scores are printed. The code should be easy to follow by now. The strings containing the scores contain some formatting information passed to d_Print() and hence the buried "\xHH" in the strings. See d_print() in display.c for a review of what codes are implemented.
And finally, messages may scroll across the bottom of the screen depending on whether the last round has been reached or if there are colony shortages (with the latter messages not added yet). The calling function must wait for all players to press their fire buttons before continuing.
And that's it!
Write games in C using Z88DK and SP1
TechnicianSi
I'm going to move this project to CVS at z88dk.org when I get a chance and get it into a (at least) partial demo state so that you can see the graphics and make tweaks to it easily.
I've just ordered a netbook so I'm thinking maybe that will create some time to work on such things during the daily commute.
Write games in C using Z88DK and SP1
I lost all the most recent code of all my zx related projects a month or so ago when my sd drive died. It was missing AI (no docs existed at that time on how the AI worked in the original) and I had not actually run the program as a whole using the graphics supplied because there was a memory problem.
The memory problem was being solved by adding / reimplementing library routines in z88dk and writing a simple and small sprite routine. sp1 does a lot of things but when a game only needs something simple, its memory footprint can get in the way. With these changes I am pretty sure the result would fit into 48k, depending on how nasty the AI got (it may or may not require a good chunk of RAM to search a decision space).
Winston also made a lot of progress on spectranet so part of the library update in z88dk was a rewrite of stdio so spectranet could seemlessly and (hopefully) efficiently be plugged into that. Mule is a great candidate for a local area network game :-)
I still have great interest in finishing this and I believe I still have a good amount of code on another computer since this project was started so long ago but it is always available time that interferes. I haven't managed to complete much in the past 2 years. I should probably reconsider my priorities :D
Write games in C using Z88DK and SP1