action games timing

Página 1/2
| 2

Por snake

Resident (53)

imagem de snake

10-11-2016, 15:31

I wanted to keep constant music playback speed even when graphics slow down (for instance: Aleste 2 with lot of sprites on screen).
Assuming that game logic + play a chunk of music + play sound effects + rendering "ordinary" frames always takes less than 1 interrupt, in case of "crowded" frames i'm thinking to split rendering across 2 interrupts, while still performing music playback each time.
Something like that:

MAIN CYCLE:
1) update logic
2) play music and effects
3) wait for vsync (through checking a flag).
4) as for ordinary frame, just render it and go back to 1; as for "crowded" frames, render a first part, then go to point 5
5) wait for vsync.
6) render the remaining part and play another chunk of music.
7) go back to point 1.

INTERRUPT:
1) set the flag.

I'm not very expert in Msx programming, probably i wrote tons of nonsense, so please, give me your advise Smile

Entrar ou registrar-se para comentar

Por Sandy Brand

Master (245)

imagem de Sandy Brand

10-11-2016, 22:16

Why not just process the music update completely inside the interrupt handler? Smile
Usually these updates should be fast enough to not cause any issues, unless you are doing some very time critical line-interrupt timings.

Por santiontanon

Paragon (1524)

imagem de santiontanon

10-11-2016, 22:55

Exactly, I was thinking the same thing. If the music and effects are handled in the interrupt, then regardless of whether the game slows down or not, music and effects will keep playing

Por NYYRIKKI

Enlighted (5889)

imagem de NYYRIKKI

11-11-2016, 09:39

No, no, no... In your mind you may think this causes cool "bullet time"-effect, but in real life it doesn't work quite like that... When player dies without seeing the bullet hitting him, he may be frustrated... When player dies because he lost sense of time he just pushes the power button and evaluates the game as "unplayable".

Without clear picture of technical details it is hard to give any exact tips as they are not always very universal. How ever the problem in your approach is that the game speed is either full speed or half speed and for player switching between them seems quite random and unpredictable. Making poor Z80 to wait is also something that you should really try to avoid... It is like taking a kid to play a football match and not giving him any time on the field to show his skills.

If i would be doing SHMUP like Aleste 2 I would be probably using screen 4 and the game engine would look something like this:

INTERRUPT:
1) Select "ready page" as current display page
2) Rotate color palette values
3) Handle input controllers
4) Handle player, enemies & bullet movement in memory
5) Do hit detection and calculate points.
6) play music
7) exit

MAIN PROGRAM:
1) output background to "VRAM write buffer"
2) output points to "VRAM write buffer"
3) output player, enemies & bullets to "VRAM write buffer"
4) set "ready page" = "VRAM write buffer"
5) "VRAM write buffer" = "VRAM write buffer" + 1 : if "VRAM write buffer" = 3 then "VRAM write buffer" = 0
6) go to step 1

If you get my idea, this way you can use all of the "extra CPU power" to draw the screen that is generally the most slow thing to do. When there is lot of stuff happening the frame rate (FPS) may drop, but the game & music speed is still constant.

Por Sandy Brand

Master (245)

imagem de Sandy Brand

12-11-2016, 00:15

@NYYRIKKI:
I don't see why you need to put all your game logic inside the interrupt handler. If you do that then your music will slow down when it takes more than 1 frame to update the entire world, which is bad.

The only thing you need in the interrupt handler is to update music + sound, page swaps, and maybe the input scanning code in order to make sure you don't miss any key up/down events. Other than that almost everything can go in the main thread (well, it depends of course how complex you want to make access to the VDP, but you catch my drift).

Por snake

Resident (53)

imagem de snake

12-11-2016, 00:47

Thanks for suggestions, soon i will try to write some code Smile
I have one doubt: i know that while performing a vdp command interrupts must be disabled. If vdp commands are in the main program, does that may cause unexpected and unwanted interrupt skipping?

Por Sandy Brand

Master (245)

imagem de Sandy Brand

12-11-2016, 01:45

There is actually no need to disable interrupts during a VDP command Smile

The only thing you need to be careful of is that, under certain conditions, modifying certain VDP registers while a copy command is being performed can cause VRAM data corruption. As far as I know this is only the case for VDP register 18, also see VDP programming guide by Grauw.

So as long as your interrupt routine doesn't do anything crazy, this should be fine (in fact, this is the best way to improve performance on your MSX by having the Z80 and VDP perform operations in parallel).

Por ARTRAG

Enlighted (6567)

imagem de ARTRAG

12-11-2016, 08:18

Grauw says:
"This issue only occurs when you have the sprites (or screen) disabled. If you use the screen position adjust register while executing a copy command and with sprites disabled, it will corrupt the byte the VDP command is currently processing. "
This is very interesting, but actually I didn't get the same result.
In my tests the missing dots were less frequent but still there...
Is there someone who tested specifically this aspect?

Por NYYRIKKI

Enlighted (5889)

imagem de NYYRIKKI

13-11-2016, 13:28

Sandy Brand wrote:

@NYYRIKKI:
I don't see why you need to put all your game logic inside the interrupt handler. If you do that then your music will slow down when it takes more than 1 frame to update the entire world, which is bad.

I did put it in there because I did put also input handling there. The game logic it self should not ever take more than 1 frame... unless you are trying to implement Angry birds type of physics calculations with complex kinetics or you are doing something horribly wrong... Usually the "expensive" task is drawing the screen, not calculating bullets and ships X & Y locations. If you move this to main program the speed differences in screen draw & logic directly affects the gameplay speed... You can solve this by adding waits that make the speed always same, but as I said... waiting is bad idea if you can avoid that. Other possibility is to add time counter to interrupt handler and use the time in game logic as input parameter... How ever this just makes the logic handling extremely complicated.

Por Sandy Brand

Master (245)

imagem de Sandy Brand

13-11-2016, 14:13

NYYRIKKI wrote:

The game logic it self should not ever take more than 1 frame... unless you are trying to implement Angry birds type of physics calculations with complex kinetics or you are doing something horribly wrong...

Hmm, well that is maybe nice in theory but in practice virtually impossible to achieve in even a moderately complex game (even without proper physics) Smile

There are so many variables at any given time: amount of enemies remaining on the screen, amount of powerups a player has collected, player might make some poor or unexpected decisions, unforeseen balancing issues, music/sound routines need varying amounts of CPU cycles depending on speed and complexity of part of a song, etc. etc. etc.

So you need to add contingency for this in your code and prioritize the most critical elements; players are much more forgiving towards subtle visual slowdowns than to audio stutters.

Either that, or you need to grossly under utilize the CPU in an attempt to be able to handle worst-case scenarios (but how do you quantify the size of such spikes with any degree of certainty?)

Just take a look at all the shoot'em-ups, for example: Gradius, Aleste, Space Manbow. They all have gameplay slowdown here and there, but the music plays at a constant rate.

Por snake

Resident (53)

imagem de snake

28-11-2016, 17:09

I agree with sandy: both logic and drawing may unpredictably slow down the game, so they should be in main cycle.

See point 2.3) http://bifi.msxnet.org/msxnet/tech/tms9918a.txt
TMS9918 requires interrupts to be disabled during two consecutive accesses to vdp port 0x99, but fortunately this doesn't seem to cause timing issues.

MAIN CYCLE:
update game logic
draw a frame
syncronize with screen refresh

INTERRUPT:
play music

This is my test code in screen 2 (SDCC, Msxdos target, no libraries required, only a suitable crt). It puts sprites, scrolls the background and plays 2 musical notes :)
Build with:
1) sdcc --code-loc 0x106 --data-loc 0 -mz80 --no-std-crt0 msxdos.rel test.c
2) hex2bin -e com test.ihx
Then put the file in a disk image with Msxdos installed.

Drawing is divided in phase 1 (1 sprite only, full speed), and phase 0 (background scroll + all 32 sprites + complex logic, very slow).
Syncronization is keep by setting and checking a flag.

------------------------------------------------------------

// io ports for vdp and psg
__sfr __at (0x98) VDP_port1;
__sfr __at (0x99) VDP_port2;
__sfr __at (0xa0) PSG_register;
__sfr __at (0xa1) PSG_value;

// vram map
#define PATTERN_GENERATOR_1 0
#define PATTERN_GENERATOR_2 2048
#define PATTERN_GENERATOR_3 4096
#define PATTERN_LAYOUT 6144
#define SPRITES_ATTRIBUTES 6912
#define PATTERN_COLOR_1 8192
#define PATTERN_COLOR_2 10240
#define PATTERN_COLOR_3 12288
#define SPRITE_PATTERNS 14336

void memcpy_reverse (void * dst, void * src, unsigned int n) __naked {
// like memcpy but the direction is downwards.
// most code from SDCC port of Solid-C libraries.
__asm
push ix
ld ix,#0
add ix,sp
ld e,4(ix)
ld d,5(ix)
ld l,6(ix)
ld h,7(ix)
ld c,8(ix)
ld b,9(ix)
pop ix
ld a,b
or c
ret z
lddr
ret
__endasm;
dst; src; n; // avoid warnings about unused variables
}

void blit (unsigned int target, void * source, unsigned int length) {
// copy ram to vram; 'target' is vram destination address
unsigned char *s;
s=(unsigned char*)source;
__critical {
VDP_port2=(char)target;
VDP_port2=(char)((target>>8)|0b0000000001000000);
}
while (length>0) { VDP_port1=*s; length--; s++; }
}

// music is stored in a vector that is scanned by a
// pointer during playback
volatile unsigned char music [256];
volatile unsigned char music_delay;
volatile unsigned char * music_position;

volatile int sync_flag;
volatile char vdp_status;

void interrupt_routine () __critical __interrupt(0)
__preserves_regs(a,b,c,d,e,h,l,iyl,iyh) {

// read the vdp status
vdp_status = VDP_port2;

music_delay++; // increment every interrupt

// Play a chunk of music every 30 interrupts.
// A chunk is a raw sequence of values to write
// on psg registers:
// register-value-register-value...
// When a register is expected but it's 255, the
// chunk ends. Two consecutive 255 are the end
// of the song (so the playback pointer rewinds).
if (music_delay>30) {
music_delay=0;
while (*(music_position)!=255) {
PSG_register=*(music_position);
PSG_value=*((music_position)+1);
music_position+=2;
}
music_position++;
if (*music_position==255) music_position=music;
}

sync_flag=1; // set the syncronization flag to 1
}

void main () {
// counters for graphics
unsigned char frame_counter=120;
char phase=0;
unsigned char scroll_phase=0;
unsigned char scroll_frame_counter=0;

// generic counters
unsigned int kk;
unsigned char k;

// sprites and background ram buffers
unsigned char pattern_buffer [768];
unsigned char sprite_buffer [256];

// music sequence. Each chunk corresponds to a
// musical note.
music_delay=0;
music_position=music;
music[0]=1; music[1]=1; music[2]=0;
music[3]=222; music[4]=8; music[5]=16;
music[6]=13; music[7]=9; music[8]=11;
music[9]=232; music[10]=12; music[11]=3;
music[12]=255;
music[13]=1; music[14]=1; music[15]=0;
music[16]=170; music[17]=8; music[18]=16;
music[19]=13; music[20]=9; music[21]=11;
music[22]=232; music[23]=12; music[24]=3;
music[25]=255; music[26]=255;

// define 1 sprite pattern
for (k=0;k<32;k++) sprite_buffer[k]=255;
blit (SPRITE_PATTERNS,sprite_buffer,32);

// define 3 background patterns
for (kk=0;kk<24;kk++) {
char c;
pattern_buffer[kk]=0b10101010;
c=(kk<8)?(0b11011010):(kk<16?0b01011001:0b01100100);
pattern_buffer[kk+24]=c;
}
blit (PATTERN_GENERATOR_1,pattern_buffer,24);
blit (PATTERN_GENERATOR_2,pattern_buffer,24);
blit (PATTERN_GENERATOR_3,pattern_buffer,24);
blit (PATTERN_COLOR_1,pattern_buffer+24,24);
blit (PATTERN_COLOR_2,pattern_buffer+24,24);
blit (PATTERN_COLOR_3,pattern_buffer+24,24);

// set screen 2 with 16x16 sprites
__critical {
VDP_port2=0b00000010; VDP_port2=0b10000000;
VDP_port2=0b11100010; VDP_port2=0b10000001;
VDP_port2=0b00000110; VDP_port2=0b10000010;
VDP_port2=0b11111111; VDP_port2=0b10000011;
VDP_port2=0b00000011; VDP_port2=0b10000100;
VDP_port2=0b00110110; VDP_port2=0b10000101;
VDP_port2=0b00000111; VDP_port2=0b10000110;
VDP_port2=0b00001111; VDP_port2=0b10000111;
}

// interrupt setup
sync_flag=0;
__critical { *((unsigned int*)57)=(unsigned int)interrupt_routine; }

/***** main cycle *****/
while (1) {
// change the phase every 120 frames
frame_counter++;
if (frame_counter>120) {
frame_counter=0;
if (phase==0) phase=1; else phase=0;
}

switch (phase) {
case 0: // heavy phase
// scroll the background every 12 frames
scroll_frame_counter++;
if (scroll_frame_counter>12) {
scroll_frame_counter=0;
scroll_phase++; if (scroll_phase>2) scroll_phase=0;
memcpy_reverse (pattern_buffer+767,pattern_buffer+735,736);
for (kk=0;kk<32;kk++) pattern_buffer[kk]=scroll_phase;
}
blit (PATTERN_LAYOUT,pattern_buffer,768);
// draw 31 sprites
for (k=1;k<32;k++) {
// each sprite plane is 4 bytes
sprite_buffer[k*4]=k*6+16; // y choordinate
if (sprite_buffer[k*4+1]>200) sprite_buffer[k*4+1]=0;
sprite_buffer[k*4+1]+=k; // x choordinate
sprite_buffer[k*4+2]=0; // sprite pattern
sprite_buffer[k*4+3]=12; // color
}
blit (SPRITES_ATTRIBUTES+4,sprite_buffer+4,124);
// simulate a complex logic :)
for (kk=0;kk<3000;kk++);
case 1: // light phase
// draw 1 sprite
sprite_buffer[0]=0; sprite_buffer[1]++;
if (sprite_buffer[1]>230) sprite_buffer[1]=0;
sprite_buffer[2]=0; sprite_buffer[3]=4;
blit (SPRITES_ATTRIBUTES,sprite_buffer,4);
break;
}

// sync_flag is 0 -> no interrupt occurred since the
// beginning of main cycle, so wait for next interrupt.
// sync_flag is 1 -> 1 or more interrupts have already
// occurred, so don't wait.
while (sync_flag==0);
sync_flag=0;
}
}

Página 1/2
| 2