Jump to content
IGNORED

some tips for coordinating sprite movement


AnalogKid

Recommended Posts

The following is a mechanism I created to control the pacing of screen elements so that your game's sprites can have 7 distinct speeds of movement.  It has minimal impact because it uses zero math and math is to be avoided at all costs because the goal of course is to keep up with the screen refresh.   When you're winning the race with the refresh your sprites move smoothly because you're in perfect sync.  Simple addition, for a counter for instance, is actually expensive for the Z80 with compiled C code, so a useful technique is to replace incrementing with bit shifting, if your maximum counter range is 7 or lower.  This coupled with logical and-ing to compare against the current counter "tick" gets you the 7 different speeds.


static const static byte HEART_BEAT[8] = {1,2,4,8,16,32,64,128};

static const byte *heartBeat = HEART_BEAT;  

static register byte currentHeartBeat;  


nmi(void)
{
  ....

  if (*heartBeat & (byte)128)
    {
      heartBeat = HEART_BEAT;
    }
  else
    {
      ++heartBeat;
    }
}


heartbeat is a pointer to the values within HEART_BEAT and in the nmi routine we nudge it forward a tick until we hit the end and start over.  Note that the first tick's value is 1 so that we can perform the logical and-ing

So now in your main play loop, 
 

static void play(void)
{
  ....
  byte motionTimer = (byte)1;

  while ((byte)1)
    {
      currentHeartBeat = *heartBeat;  // the nmi function updates what heartBeat is pointing to


      if (motionTimer & currentHeartBeat)
        {
           .......

           motionTimer = getTimer((byte)3); // reenter the motion code on the third tick after the current one
        }

 

and the method to get the value for the desired heartbeat increment, taking values between 2 and 7 inclusive


static inline byte getTimer(const byte ticks)
{
  return (byte) ( (currentHeartBeat << ticks) | (currentHeartBeat >> ((byte)8 - ticks) ) );   
}


note: of course screen elements that should move on every refresh, like bullets, etc. don't need the timer code at all.  Also, you might want to pass a variable to getTimer, allowing you to speed up the characters in later rounds

For the case of the player's character, because you don't want a delay for the motion to start when the player presses a button or pushes the joystick, the timer should start at 0 and be reset to 0 when the user is no longer providing the input


byte moveLeftTimer = (byte)0;


.....


if ((byte)LEFT & j)
  {        
    if (!moveLeftTimer || (moveLeftTimer & currentHeartBeat))
      {
        ...........
        moveLeftTimer = getTimer((byte)2);
      }
  }
else
  {
    moveLeftTimer = (byte)0;
  }
            


If a character needs to move more slowly than 8.5 times a second, you can add "multipliers", like a flip-flop variable to reduce the rate in half:


byte bossMoveFlag = (byte)0;
....


if (bossMotionTimer & currentHeartBeat)
  {
    if (bossMoveFlag)
      {
        .......
      }
    bossMoveFlag = !bossMoveFlag;
    bossMotionTimer = getTimer((byte)7);  // values 4 to 7 now get tick counts of 8 to 14
  }


Lastly, you need a flag to keep the nmi and your main loop in sync because if you're ahead of the refresh, you'll get jittery characters the same as you would as if you were lagging.  I think there might be a register that can be used for this but the comment I saw about it was vague as to when it's set and reset, so for now I'm using my own volatile variable
 

static volatile byte renderSyncFlag = (byte)0;    // coordinates the game loop and the nmi interupt handler


void nmi(void)
{
  ....
  renderSyncFlag = (byte)0;
}


static void play(void)
{
  ....

  while ((byte)1)
   {
     renderSyncFlag = (byte)1;

     .....

     while (renderSyncFlag)
       {    
         //wait for nmi to flip the flag
       }
   }

    


To finish, here are some tips to help you keep head of the refresh, because you should code your C like the compiler/optimizer wants you to.  Yes, the code will be ugly and it will feel dirty at first


Avoid putting code in subroutines unless it's to avoid blocks of duplicate code.  Jumping, pushing, popping, stack usage, etc. all come at a cost, plus you can reuse local variables and the most common ones that are used repeatedly, like "x" and "y", they can be given the register declaration


Put performance critical variables at the end of the declaration list


Make functions static


Include prototypes for ALL fuctions at the top of your file.  This is necessary to avoid numeric arguments being up-casted (don't ask me why)


The variables for your main game loop should be static globals or at least declared as static 


Unravel your own critical loops: all looping costs, if your critical loop code is small, unravel it


cast all constants to byte  


put constants before variables:  if ((byte)1 > x)  .....  i = (byte)5 + j;


also for math, cast cast cast:    i = (byte)((byte)5 + (byte)j);         // numbers get upgraded to 16 bit integers so cast them down as much as possible, don't force the compiler make assumptions


make switch statment cases numerically ordered when possible, even if it means a little duplication


replace counters looping from 1 to a value less than 8 with bit shifting


when you do need a for loop, count down rather than up


replace multiplication by 2,4,8, etc. with bit shifting (some optimizers do this anyway)


replace i++; with ++i;  etc. for all the operators


I'm told that compilers like if-then logic like  i = j ? 1 : 2;    because it's as basic as it gets


replace calculations or complex or bulky logic with lookup tables in ROM, i.e. const arrays of values 


avoid floats like the plague, again use lookup tables when you can, like for cosine and sine values


an efficient looping technique is to make your character structs linked-list nodes because it's slightly faster to work with pointers than arrays and arrays take up more RAM because they are indexed by integers (I'll cover this in another post)


don't perform collision detection on every single refresh - if characters are 16x16 and moving 1 pixel at a time, it's really not conceivable that you're going to miss detecting a collision if you only check every third refresh


This is the first of a series of tips to help folks starting out in C coding of Colecovision games.  I hope they help in bringing your ideas to the screen

Edited by AnalogKid
  • Thanks 1
Link to comment
Share on other sites

My gameplay loop contains,

 

while(game==1){

delay(1);//this is the heartbeat since it wait til the screen signal that it has finish drawing the screen.

updatesprites(0,64);
//any graphic that need to be updated like score and definable sprite here

controls();

ObjectBehavior();//apply value to sprites

Spriterotation();//shuffle sprites every frames, so if there's 4 sprites on a line, it'll flicker

//box collision stuff, gotta do them in software,which eats the most cpu cycle.

}

If I think the game can't run at 60 fps consistently, I'll drop it to 30 fps like,
while(game==1){

delay(1);
updatesprites(0,64);
Spriterotation();
delay(1);

updatesprite(0,64);

Spriterotation();

}

It's not the lag is the problem, it's the nmi vram corruption too.  You have to make sure to do graphic stuff right after delay(1), not towards the end of the cycles.

I avoid using non 8-bit variable and non powered by 2 multiplication and division if possible. 

Link to comment
Share on other sites

22 hours ago, AnalogKid said:

replace i++; with ++i;  etc. for all the operators

While this is good general practice in C++, it has no impact on integer types - both forms will generate the same assembly code if the result is not being used immediately. I wouldn't waste time searching code to change it, just keep it in mind AS you code.

 

You have some good descriptions at the top of your post, but you sort of cheap out at the end there. Go ahead and expand with why the tips are useful, so that people can logically decide when to use them as opposed to blindly following them. In this case - it has to do with the return value of the expression. When you use "++i", the return value is after the increment. When you use "i++", the return value is before the increment, meaning a temporary copy of the value needs to be saved.

 

Tips of my own for people:

 

Learn to read the assembly output of the compiler. This will give you tons of insight into what the compiler does with particular expressions and why things may be faster or slower than others. You will be shocked at how much work some simple sequences take, and might decide to avoid them. It will also help you spot bugs. As a side benefit, you'll start to get familiar with assembly and might start to write some of your own here and there. ;)

 

As for the NMI, I don't like relying on timing. When your code only works when it's fast enough, that's called a "race condition", and it leads to difficult to track bugs. I've got other threads where I've talked about my preferred way to tame the NMI, but the simplest way is to split up your code, and either do all VDP work on the NMI, or do no VDP work on the NMI (ie: just set a flag). The NMI can not fire again until you read the status register, so if you don't read the status register until all VDP work is finished, you can never collide with the NMI.

 

The number one rule of optimization, though, is test it. That's the only way to actually know. ;)

 

Link to comment
Share on other sites

22 hours ago, Tursi said:

 

As for the NMI, I don't like relying on timing. When your code only works when it's fast enough, that's called a "race condition", and it leads to difficult to track bugs. I've got other threads where I've talked about my preferred way to tame the NMI, but the simplest way is to split up your code, and either do all VDP work on the NMI, or do no VDP work on the NMI (ie: just set a flag). The NMI can not fire again until you read the status register, so if you don't read the status register until all VDP work is finished, you can never collide with the NMI.

 

 

+1 !?

 

Put all your VDP work in the NMI routine, it solved lot of headache for me!.  :D

Link to comment
Share on other sites

43 minutes ago, youki said:

+1 !?

 

Put all your VDP work in the NMI routine, it solved lot of headache for me!.  :D

I did the same.  For the intermittent video updates I used flags to "queue" updates so all VRAM work could be done in the interrupt handler.  I've been working in event-queue driven paradigms for so very long it's against my nature to try to control the flow.  I let the flow be in charge and work with it.  Game coding has always been about "racing the beam" and for me that's where the fun is.  The Atari/Activision developers were absolute geniuses as they didn't have the huge 18ms window we have, they had however long it took for the beam to move from the end of one scan line to the start of the next to perform any computation.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...