Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » Sprite Movement

This thread is locked; no one can reply to it. rss feed Print
Sprite Movement
C Bird
Member #6,854
February 2006

Hi, I'm not sure if this is the correct part of the forum for this type of question, but I received amazing support with my previous 'magic pink' question, so I thought I'd dump this here.
What is the best way to take keyboard commands and turn them into sprite movements. Take tetris for example, only instead of rotating the blocks, you only need to move them from right to left. How would one go about doing this?
Here are some ideas I've had, I know they are crummy, but I'm having a major brain-fart here.???

1void poll_user_input()
2{
3 // The blocks are 64x64
4 // I assume you would need to set the initial (X,Y)
5 // value before attempting any movement. Vertical
6 // movement is handled by the engine, the blocks move
7 // down 64pixels every second on standard mode.
8 if(key[KEY_RIGHT])
9 {
10 block.posY = block.posY + 64
11 }
12 else if(key[KEY_LEFT])
13 {
14 block.posY = block.posY - 64
15 }
16}

As you can clearly see, I'm having a hard time grasping how to set and move a 'tile/block'. If anyone would care to drop some pseudo, I'd be MUCH appreciative!

Birdeeoh
Member #6,862
February 2006
avatar

Without elaborating alot, which I am prone to do, I'll just say - yes, that's a common way to do it. With one correction...

1void poll_user_input()
2{
3 // The blocks are 64x64
4 // I assume you would need to set the initial (X,Y)
5 // value before attempting any movement. Vertical
6 // movement is handled by the engine, the blocks move
7 // down 64pixels every second on standard mode.
8 if(key[KEY_RIGHT])
9 {
10 block.posY = block.posY + 64
11 }
12 if(key[KEY_LEFT])
13 {
14 block.posY = block.posY - 64
15 }
16}

Notice I removed the "else" on the second if. Consider this scenario - the player is pressing both left and right at the same time. With your version, only the "right" would be detected and the block would move right. In reality, I think a more... "expected" behavior would be that left and right balance each other out and the block shouldn't move.

Keep in mind after you do all the movement based on the keyboard state you'll need to do bounds checking, of course.

Good luck!

[url http://developer.berlios.de/projects/openlayer/]OpenLayer[/url is an OpenGL accelerated 2D library for fast and easy graphics development under Allegro

Elverion
Member #6,239
September 2005
avatar

Also, you shouldn't specifically say "block.posY = block.posY + 64", but use a constant for 64. In the header file, put "#define BLOCK_SIZE 64" or something similar. Then you can simply use "block.posY = block.posY + BLOCK_SIZE", which is the prefered way to do it. It prevents minor errors such as typos (63 or 65 instead of 64 would cause quite a problem in your case), and is very useful if you ever wish to change the block size. You would then only need to change it in one location (the #define) rather than possibly 100s.

Aside from that, as Birdeeoh said, you need to do bounds checking. I prefer to do pre-checking, though, when available.

psuedo code:

if( key[KEY_LEFT} and ( X - BLOCK_SIZE > left boundary ) ) {
  X = X - BLOCK_SIZE; }

if( key[KEY_RIGHT] and ( X + BLOCK_SIZE < right boundary ) ) {
  X = X + BLOCK_SIZE; }

--
SolarStrike Software - MicroMacro home - Automation software.

C Bird
Member #6,854
February 2006

Birdeeoh:
I did actually have dual if's in my code instead of the if-else, I have no idea why I put that.

I believe in my original post I had my X's and Y's mixed up. If I want horizontal movement, that is moving it from side to side, that is the X axis right?

I'm glad to see that I at least have some idea of what I'm trying to accomplish here. Bounds checking was something that naturally followed keyboard input. In my actual code i have it in the same function.
example:

1 
2#define LEFT_BOUNDS 256
3#define RIGHT_BOUNDS 704
4#define BLOCK_SIZE 64 // thanks for the tip Elverion
5 
6void poll_user_input()
7{
8 if(key[KEY_RIGHT])
9 {
10 block.posX = block.posX + BLOCK_SIZE
11 if(block.posX - BLOCK_SIZE < RIGHT_BOUNDS)
12 {
13 block.posX = block.posX + BLOCK_SIZE;
14 }
15 }
16 if(key[KEY_LEFT])
17 {
18 block.posX = block.posX - BLOCK_SIZE // thanks for the tip Elverion
19 if(block.posX - BLOCK_SIZE > RIGHT_BOUNDS)
20 {
21 block.posX = block.posX - BLOCK_SIZE;
22 }
23 }
24}

Birdeeoh
Member #6,862
February 2006
avatar

On second inspection yes you had x and y axis mixed up. ;) I wasn't paying attention to that detail because you had the gist of it correct!

In the followup code you posted, I have a reccommendation, as well as a question about a few possible typos...
For key[KEY_RIGHT] you have the check
if(block.posX - BLOCK_SIZE < RIGHT_BOUNDS)
and for key[KEY_LEFT] you have
if(block.posX - BLOCK_SIZE > RIGHT_BOUNDS)
I think you're missing a LEFT_BOUNDS and/or have your greater/less than signs backwards... ;)

But my general reccommendation would be to do the whole thing this way -

1void poll_user_input()
2{
3 /* Keyboard Check */
4 if(key[KEY_RIGHT])
5 block.posX = block.posX + BLOCK_SIZE
6 if(key[KEY_LEFT])
7 block.posX = block.posX - BLOCK_SIZE
8
9 /* Bounds Check */
10 if( block.posX < LEFT_BOUNDS )
11 {
12 block.posX += BLOCK_SIZE;
13 }else if( block.posX > RIGHT_BOUNDS)
14 {
15 block.posX += BLOCK_SIZE;
16 }
17}

Of course the actual check may be more like
if( block.posX <= LEFT_BOUNDS )
depending on how you actually define your boundries.

For me this is cleaner stylistically/logically, but whatever makes sense to you! ;)

[url http://developer.berlios.de/projects/openlayer/]OpenLayer[/url is an OpenGL accelerated 2D library for fast and easy graphics development under Allegro

Jonny Cook
Member #4,055
November 2003

That's going to work, but it it might move the blocks too fast. Perhaps you could do it like this:

if (keypressed()) {
    switch (readkey() >> 8) {
        case KEY_LEFT:
            // do whatever...
            break;

        case KEY_RIGHT:
            // do whatever...
            break;
    }
}

That will make it so there's a slight delay before each keypress is registered, kind of like there is when you hold down a key in a text box.

The face of a child can say it all, especially the mouth part of the face.

Birdeeoh
Member #6,862
February 2006
avatar

That's going to work, but it it might move the blocks too fast.

My first thought was "what? are you crazy?" then I realized we're talking about a tile-based coordinate system and the example given was tetris style. In that case, indeed moving the block one space over every single frame would be WAY too fast ;)

However your approach lends itself to a problem - have you ever played a tetris-like game where you hold the key down and you KNOW they are relying on the key-repeat because there's a long delay before the block starts moving?

To do this, you'll need to set it up properly first. Perfect function, Allegro has -
void set_keyboard_rate(int delay, int repeat);
Tinkering around for the right repeat rate is up to the programmer. But the delay should most definitely be zero - that is, for a Tetris-type application.

I'd have to make a slight mod to the given code though -

while( keypressed() ) 
{
    switch (readkey() >> 8) 
    {
        case KEY_LEFT:
            // do whatever...
            break;

        case KEY_RIGHT:
            // do whatever...
            break;
    }
}

Notice the big difference between if( keypressed()) and while( keypressed()) - it's very possible the user could press two keys, press the same key twice, or the repeat rate will put two keys in the buffer inbetween two distinct frames. If you only handle one keypress per frame the buffer is going to back up. Gotta empty it out every iteration of your main loop ;)

Good catch Jonny - I wasn't even thinking about the specific application (assuming it is actually tetris-like)

[url http://developer.berlios.de/projects/openlayer/]OpenLayer[/url is an OpenGL accelerated 2D library for fast and easy graphics development under Allegro

C Bird
Member #6,854
February 2006

Birdeeoh:
I'm dumb!, yes i did get those all muddled up, i was thinking too hard about if i was supposed to be using X's or Y's.. in programming sense... i don't think X's and Y's matter, only what u assign to them, but i guess its best not to confuse myself any further on that matter. :)

As an answer to the 'question' yea it is very much like a tetris clone. More like super puzzle fighter (if you've ever had the pleasure of playing that great arcade game). With the bounds checks you've all been so kind to post previously, which one would work best for making sure that as soon as the edge of the block touches the 'wall' (right or left boundary), it won't let the block precede any further.

Dustin Dettmer
Member #3,935
October 2003
avatar

Quote:

If you only handle one keypress per frame the buffer is going to back up. Gotta empty it out every iteration of your main loop ;)

I don't think the keyboard can send anywhere near 50 key presses per second...

Birdeeoh
Member #6,862
February 2006
avatar

AT and original PS/2 keyboards were hardware limited by the mobo's keyboard controller. Newer PS/2 controllers and (all?) USB keyboards are 100% interupt driven and most of the keyboard handling is by the Operating System.

In Windows the max rate configurable by the Keyboard Control Panel is 30 cps, but with dozens of keyboard manufacturer utilities, 3rd party utilities, and hacking the registry you can crank that WAY up.

And programatically things can get pretty insane - I just ran a simple test with set_keyboard_rate( 1, 1 ), meaning the repeat starts after 1 ms and the key repeats each ms. I ran a two second test and grabbed 2001 keypresses. The 1000 cps limit is due to Allegro's handling of it - it can be higher at the OS level.

But the point isn't neccessarily the insane rates POSSIBLE with repeating one key so much as multiple key presses. If you mash the keyboard you can easily get more than 1 keypress in, say, 1/60th of a second. Even with just two fingers on two different arrow keys it can easily happen.

Plus other programs can be inserting keys into the buffer programatically...

I suppose the whole point is that it's just best to program for what CAN happen rather than what you expect to happen because both the real world environment and the user will violate your expectations ;)

[url http://developer.berlios.de/projects/openlayer/]OpenLayer[/url is an OpenGL accelerated 2D library for fast and easy graphics development under Allegro

Elverion
Member #6,239
September 2005
avatar

Interesting discussion....Birdeeoh, can you explain to me what "readkey() >> 8" really means and how it works in the code you had posted earlier? I understand how that shifts the value of what readkey() returns so that it returns the scancode (or whatever)...but, what is this about the delay and such?

C Bird, X generally represents horizontal positions, and Y represents vertical positions. Z would be depth, but in 2D there is no depth. Although, if you really wanted to mix up your variable names, yes, you could youse Y to represent the horizontal position, and X for vertical, but that's a bad practice. As for the "best" method of preventing the block from going out of bounds, I would suggest a pre-test (as I posted code to above). A post-test also could work, but I think it makes more sense to prevent the block from moving outside boundaries, than allowing it to and snapping it back into boundaries. Either way, this would happen so fast that the user would not notice it had gone out of boundary. It's all up to you and what you feel more comfortable with.

--
SolarStrike Software - MicroMacro home - Automation software.

Birdeeoh
Member #6,862
February 2006
avatar

Elverion

readkey() returns an int with two pieces of data in it. The lowest byte is the ASCII code of the keypress that was in the keyboard buffer. This is the char that you would put in a c-style string (ie a const char*)

The next byte is the actual scancode read off the keyboard. You access the second byte by shifting the integer over 1 byte, hence the readkey() >> 8

The scancode differs from the ASCII code in that the scancode represents a key on the keyboard whereas the ASCII char represents a (usually) printable character. IE KEY_B is the scancode but the char is either 'b' or 'B' depending on if shift or caps lock was active when KEY_B was entered into the buffer.

Now the whole keypressed() and readkey() system refers to the keyboard buffer. A keypress is entered into the buffer whenever someone presses and then releases a key. This is the behavior you can capture by hooking into the keyboard_callback function.

But if you press a key and hold it down, the operating system takes over and starts generating repeated keypresses for that character. For every repeat the OS decides on, it enters that key into the keyboard buffer as if the user had pressed and released the key.

There's two aspects to the key repeat - the delay and repeat rate. The delay is how long you have to hold down the key before the OS starts generating repeats. The rate is... the rate! How many keypresses the OS generates per second.

I think a common setup in the Windows control panel is a 250 ms delay and 30 repeats per second. But using set_keyboard_rate( delay, rate ) in your Allegro app you can customize it to a wide range of possibilities.

God, I type too much. Hopefully that answered your question.

[url http://developer.berlios.de/projects/openlayer/]OpenLayer[/url is an OpenGL accelerated 2D library for fast and easy graphics development under Allegro

Elverion
Member #6,239
September 2005
avatar

Thanks. I learned a little bit from you there, although it was a little bit more than needed :p Based on what you had said earlier, I thought that you were saying readkey() automatically included some sort of delay mechanism (other than the key_up thing). Thanks for clarifying though.

--
SolarStrike Software - MicroMacro home - Automation software.

Tobias Dammers
Member #2,604
August 2002
avatar

I don't the the keypressed() approach is such a good idea; usually, you want to increase movement speed with level, based on a game-logic-specific variable. Since the keyboard routines are part of the input API, you'll need 2 function calls to set a new game speed (e.g. when going up one level). A better solution would be to implement the key repeat manually by using a counter. Something like this:

1int keyb_counter = 0;
2int keyb_delay = 10; // 1 movement per 10 logic ticks. Set
3 // this to an appropriate value to adjust speed.
4 
5void logic_update() {
6 keyb_counter = MIN(keyb_delay, keyb_counter + 1);
7 if (key[KEY_LEFT] && !key[KEY_RIGHT]) {
8 if (keyb_counter >= keyb_delay) {
9 move_left();
10 keyb_counter = 0;
11 }
12 }
13 if (key[KEY_RIGHT] && !key[KEY_LEFT]) {
14 if (keyb_counter >= keyb_delay) {
15 move_right();
16 keyb_counter = 0;
17 }
18 }
19 // and more logic code...
20}

This way, you avoid any and all hardware issues involved when adjusting the key repeat rate.

---
Me make music: Triofobie
---
"We need Tobias and his awesome trombone, too." - Johan Halmén

Birdeeoh
Member #6,862
February 2006
avatar

For one, tetris-style games don't ALWAYS change the speed as the level advances. I remember many a tetris clone where I could rely on the key-repeat action at earlier levels but at later levels I have to start going into turbo-key-press mode manually. And plenty where the auto-repeat was too fast for earlier levels but the only thing to rely on later ;)

But if this is a desirable thing for the programmer...

I always cringe a little regarding locking anything into the logic frame rate without taking into account that the logic rate can be quite dynamic over the course of execution =/

set_keyboard_rate() is such an inexpensive call, it may be worth looking into.

But, I agree a custom delay is quite reasonable - for a case this simple I'd probably hook into keyboard_lowlevel_callback and keep my own motion flag

[url http://developer.berlios.de/projects/openlayer/]OpenLayer[/url is an OpenGL accelerated 2D library for fast and easy graphics development under Allegro

C Bird
Member #6,854
February 2006

Ok, so from all the advice and corrections i've been given, here is what i've pieced together from the submitted code:

1// movement.h
2// With X/Y corrections by Elverion.
3#ifndef _MOVEMENT_H_
4#define _MOVEMENT_H_
5 
6#define BLOCK_SIZE 64
7#define L_BOUNDS 192
8#define R_BOUNDS 704
9 
10void poll_user();
11 
12void poll_user()
13{
14 while(keypressed())
15 {
16 switch(readkey() >> 8)
17 {
18 case KEY_LEFT: // Left Arrow Has Been Pushed
19 block.y = block.y - BLOCK_SIZE;
20 if(block.y < L_BOUNDS)
21 {
22 // If the block is now sitting past
23 // the left wall, move it back
24 block.y = block.y + BLOCK_SIZE;
25 }
26 break;
27 case KEY_RIGHT: // Right Arrow Has Been Pushed
28 block.y = block.y + BLOCK_SIZE;
29 if(block.y > R_BOUNDS)
30 {
31 // If the block is now sitting past
32 // the right wall, move it back
33 block.y = block.y - BLOCK_SIZE;
34 }
35 break;
36 }
37 }
38}
39#endif // _MOVEMENT_H_

Ok, so i've got a square moving from side to side on my screen, but he's not disappearing from his previous locations. How do i go about clearing his trails, but keeping all the previously dropped squares where they lay?

Onewing
Member #6,152
August 2005
avatar

Quote:

Ok, so i've got a square moving from side to side on my screen, but he's not disappearing from his previous locations. How do i go about clearing his trails, but keeping all the previously dropped squares where they lay?

Are you drawing your sprite directly to the screen?

There are several different ways to draw/output your graphics (e.g. double buffering, page flipping, tripple buffering, etc.). Double buffering is pretty easy to understand. In a few words, you draw everything to the buffer after all your logic occurs each game loop and then draw the buffer to the screen. Before you start drawing to the buffer, do:

clear(buffer)

And then continue your drawing. Make sense?

------------
Solo-Games.org | My Tech Blog: The Digital Helm

C Bird
Member #6,854
February 2006

what i have drawing wise, is this:

1 
2BITMAP *buffer;
3 
4void draw_block() // Called After a Tile Has Moved
5{
6 // ignore any weird assignments please
7 draw_sprite(buffer, (BITMAP*)dat[CURRENT_BLOCK].dat, currentBlock.x, currentBlock.y);
8}
9 
10void draw_screen() // Called Last
11{
12 acquire_screen();
13 blit(buffer, screen, 0, 0, 0, 0, currentBlock.x, currentBlock.y);
14 release_screen();
15}

how well do u think this will fair in this particular project. So let me get this straight. I should be doing it like this?

void draw_block() // do this while moving the block
{
     draw_sprite(buffer, BITMAP, b.x, b.y);
     clear(buffer);
}
void draw_screen() // do this when you've finished final block movement
{
     acquire_screen();
     blit(buffer, screen, 0, 0, 0, 0, b.x, b.y);
     release_screen();
}

Onewing
Member #6,152
August 2005
avatar

Quote:

how well do u think this will fair in this particular project.

You are the programmer, how well did it fair to you?

Here's my 2 cents:

1 
2//Main loop
3while(!key[KEY_ESC]) //or however you want the loop to exit
4{
5 // Do game logic here (like movement and whatnot)
6 ...
7 
8 // Now draw everything to the buffer
9 clear(buffer);
10 // run all draw routines
11 ...
12 
13 //Now draw buffer to screen
14 draw_sprite(screen, buffer, 0, 0);
15}

Remember, clear(buffer) clears the buffer, so doing clear after drawing something to the buffer will nullify that drawing.

------------
Solo-Games.org | My Tech Blog: The Digital Helm

C Bird
Member #6,854
February 2006

ok, i don't think i got this part:

    // Now draw everything to the buffer
    clear(buffer);
    // run all draw routines

so you drew to the buffer and than cleared it w/o first blitting it to the screen? for some reason that doesn't make any sense to me, i thought you would draw to the buffer, blit the buffer to the screen, and then clear the buffer out.

Birdeeoh
Member #6,862
February 2006
avatar

Not the most efficient way but the easiest way would be to store the location of each fallen block or store a 2d array representing the tilemap that is the game board and which spaces are filled.

How else will you do "hit detection" with the currently active block if you don't know the layout of the gameboard?

Then each refresh cycle its quite easy to just redraw the entire gameboard - speed shouldn't be an issue with a game this simple, and with double buffering/page flipping you won't see any screen artifacts.

[url http://developer.berlios.de/projects/openlayer/]OpenLayer[/url is an OpenGL accelerated 2D library for fast and easy graphics development under Allegro

Onewing
Member #6,152
August 2005
avatar

Quote:

i thought you would draw to the buffer, blit the buffer to the screen, and then clear the buffer out.

My pseudocode was somewhat misleading. I clear the buffer, draw to the buffer, then blit to the screen. My code:

 // Now draw everything to the buffer
    clear(buffer);
    // run all draw routines

meant this: "Now draw everything to the buffer" was meant to be a comment, not a command, "clear(buffer)" means clear(buffer), and "run all draw routines" means draw everything you are going to draw to the buffer at this point.

------------
Solo-Games.org | My Tech Blog: The Digital Helm

C Bird
Member #6,854
February 2006

Steven Silvey:
ahh, makes much more sense now, hehe. Sorry about that. So this is how i should run my drawings (roughly):

// clear the buffer each pass
clear(buffer)
// draw the sprite to the clean buffer
draw_sprite(buffer, currrentBlock, currentBlock.x, currentBlock.y)
// update the screen with the buffer
blit(buffer, screen, 0, 0, 0, 0, SCREEN_W, SCREEN_H)

so here is my modified code:

1/////////////////////////////////////////////////////////////////
2// game.h ///////////////////////////////////////////////////////
3#define BLOCK_SIZE 64
4#define R_BOUNDS 704
5#define L_BOUNDS 192
6BITMAP *buffer;
7// Function Declarations
8void user_poll();
9void update_screen();
10// Function Definitions
11void user_poll()
12{
13 if(key[KEY_LEFT])
14 {
15 if(block.x + BLOCK_SIZE < R_BOUNDS)
16 {
17 block.x = block.x + BLOCK_SIZE;
18 }
19 else
20 {
21 // play sound that indicates a failure to move
22 }
23 }
24 if(key[KEY_RIGHT])
25 {
26 if(block.x - BLOCK_SIZE > L_BOUNDS)
27 {
28 block.x = block.x - BLOCK_SIZE;
29 }
30 else
31 {
32 // play sound that indicates a failure to move
33 }
34 }
35 // other bounding checks go here
36 draw_sprite(buffer, currentBlock, currentBlock.x, currentBlock.y);
37}
38void update_screen()
39{
40 blit(buffer, screen, 0, 0, 0, 0, buffer->w, buffer->h);
41 clear(buffer);
42}
43/////////////////////////////////////////////////////////////////
44// main.cpp//////////////////////////////////////////////////////
45#include "game.h"
46...
47while(!key[KEY_ESC])
48{
49 poll_user();
50 update_screen();
51}
52...

Go to: