- Handling bullets in a SHMUP-ish way in Axe
- 13 Nov 2013 11:46:29 am
- Last edited by matrefeytontias on 14 Nov 2013 02:00:12 pm; edited 1 time in total
Ever wondered how SHMUP (SHoot-theM-UP) could handle so many bullets while remaining so smooth ? No ? Well, I'll explain it anyway
Let's say we have a 100-bytes free RAM area ; you want to make a bullet table out of it. A bullet/entry is 5 bytes long, and defined as follows :
- Byte 0 : is the bullet active ? Understand : has it been shot / does it need to be drawn ?
- Byte 1 : X coordinate of the bullet
- Byte 2 : Y coordinate of the bullet
- Byte 3 : X speed of the bullet
- Byte 4 : Y speed of the bullet
Let's write a small SHMUP simulator. Nothing too fancy, only a ship shooting bullets, to demonstrate how it can be done using plain free RAM and with as few code as possible.
First, we need a ship. Take the sprite you want, just make sure it's 8*8 (or you'll have to handle the drawing yourself, which is not a big deal anyway). I'll go with [1028448200000000]. Let's say the coordinates of our ship is (X, Y). Let's write the necessary to move our ship around (don't bother about the ship going off-screen, it's not important here).
Code:
Now, we need to actually shoot the bullets. Since we have a 100-bytes free RAM area and each bullet/entry is 5 bytes long, we can have at most 20 bullets at once on the screen. Let's initialize the bullet table (put this code before the "While 1" block), using L1 as a free RAM area (it's 768-bytes long, but let's say it's 100) :
Code:
This code sets the 100 first bytes of L1 to 0. This is to make sure no bullet is shot before we actually want to shoot. Why will doing that change anything ? Remember we defined our first byte as a flag to say whether or not a bullet is active. This line sets this byte to 0 for each and every possible entry, so that no bullet is being active.
Now, all the following code will go in the "While 1" block, this is where we will actually handle _all bullet movement_. Yes, all. You'll see that it's not complicated at all.
First, we go through each and every bullet of the table. Since a bullet entry is 5-bytes long and we have 100 free bytes, we need to advance 5 bytes by 5 bytes through 20 bullets.
Code:
Now that we selected our current bullet (B now holds the address of the first byte of the Ath bullet of the table, starting at 0), we can start working on it. Actually wait ! We have to test the first byte to see if the bullet is being active or not. We don't want to waste any CPU time on an inactive bullet !
Code:
Now, we can apply things on our bullets while being sure they are active. But what do we want to do to these bullets ? The answer is simple : make them go forward. Actually no, not forward, but in the direction given by the X speed and Y speed of every bullet. So what we do is simply adding the X speed to the X coordinate and the Y speed to the Y coordinate. By doing that, we actually move our bullet like we wanted to.
Code:
Now that our bullet is moved, we need to display it on the screen. But wait a second ! Since it has moved, we must make sure it still is on the screen ! Let's add some tests to that code :
Code:
What do we want to do depending on the result of this test ? Simple enough : if the bullet is on-screen, display it with a Pt-On, else simply make it inactive by setting its first byte to 0, so it will be ignored on the next game loop iteration.
Code:
Now, we have the bullet handling code done. Yeah, that was really all ! But hang on a second, the whole program is not done yet. We still have to make the ship shoot, or we'll just stay with a movable ship and a bullet handler that handles nothing.
So what we want to do is : each time the user presses [2nd], one bullet is fired from the ship. The bullet must go up (Y speed will be -2) and at the left or right (X speed will be either -1, 0 or 1, chosen randomly). To do so, we must keep track of which bullet/entry has been used in the table, and which are free.
So we'll use another variable which will vary between 0 and 19 inclusive and which will be the current entry in the bullet table - the bullet that can be shot. Each time we will shoot, we will increment that variable, and when it reaches 20, we set it back to 0 (because we can shoot 20 bullets). We will use that variable as an index in the bullet table. What we also want is a timer, to prevent the player to shoot too fast, because then the game becomes too easy. Each time we shoot, we set that timer to 8, and only shoot if that timer is 0, so that the player can only shoot every 8 frames.
First, we must set our variables to 0 (outside the game loop). Let's use C as the bullet counter and T as the timer.
Code:
Now, we can write our shooting code. Add it in the game loop, just before the "EndIf getKey(15)" line.
Code:
And now we're done ! Yeah, already ! Here's the full code :
Code:
If you try the code, you'll see that this is going a little too fast, so I put a Pause 20 in that screenshot, right after the DispGraphClrDraw command :
Have fun with that technique, and don't hesitate to ask questions if you don't understand something
EDIT : fixed the issue Charty pointed out
Let's say we have a 100-bytes free RAM area ; you want to make a bullet table out of it. A bullet/entry is 5 bytes long, and defined as follows :
- Byte 0 : is the bullet active ? Understand : has it been shot / does it need to be drawn ?
- Byte 1 : X coordinate of the bullet
- Byte 2 : Y coordinate of the bullet
- Byte 3 : X speed of the bullet
- Byte 4 : Y speed of the bullet
Let's write a small SHMUP simulator. Nothing too fancy, only a ship shooting bullets, to demonstrate how it can be done using plain free RAM and with as few code as possible.
First, we need a ship. Take the sprite you want, just make sure it's 8*8 (or you'll have to handle the drawing yourself, which is not a big deal anyway). I'll go with [1028448200000000]. Let's say the coordinates of our ship is (X, Y). Let's write the necessary to move our ship around (don't bother about the ship going off-screen, it's not important here).
Code:
:[1028448200000000]→Pic1
:0→X→Y
:
:While 1
:Pt-On(X,Y,Pic1)
:
:DispGraphClrDraw
:getKey(3)-getKey(2)+X→X
:getKey(1)-getKey(4)+Y→Y
:EndIf getKey(15)
Now, we need to actually shoot the bullets. Since we have a 100-bytes free RAM area and each bullet/entry is 5 bytes long, we can have at most 20 bullets at once on the screen. Let's initialize the bullet table (put this code before the "While 1" block), using L1 as a free RAM area (it's 768-bytes long, but let's say it's 100) :
Code:
:Fill(L1,100,0)
This code sets the 100 first bytes of L1 to 0. This is to make sure no bullet is shot before we actually want to shoot. Why will doing that change anything ? Remember we defined our first byte as a flag to say whether or not a bullet is active. This line sets this byte to 0 for each and every possible entry, so that no bullet is being active.
Now, all the following code will go in the "While 1" block, this is where we will actually handle _all bullet movement_. Yes, all. You'll see that it's not complicated at all.
First, we go through each and every bullet of the table. Since a bullet entry is 5-bytes long and we have 100 free bytes, we need to advance 5 bytes by 5 bytes through 20 bullets.
Code:
:For(A,0,19)
:A*5+L1→B
:
:End
Now that we selected our current bullet (B now holds the address of the first byte of the Ath bullet of the table, starting at 0), we can start working on it. Actually wait ! We have to test the first byte to see if the bullet is being active or not. We don't want to waste any CPU time on an inactive bullet !
Code:
:For(A,0,19)
:A*5+L1→B
: If {B}
:
: End
:End
Now, we can apply things on our bullets while being sure they are active. But what do we want to do to these bullets ? The answer is simple : make them go forward. Actually no, not forward, but in the direction given by the X speed and Y speed of every bullet. So what we do is simply adding the X speed to the X coordinate and the Y speed to the Y coordinate. By doing that, we actually move our bullet like we wanted to.
Code:
:For(A,0,19)
: A*5+L1→B
: If {B}
: {B+1}+sign{B+3}→{B+1}
: {B+2}+sign{B+4}→{B+2}
:
: End
:End
Now that our bullet is moved, we need to display it on the screen. But wait a second ! Since it has moved, we must make sure it still is on the screen ! Let's add some tests to that code :
Code:
:For(A,0,19)
: A*5+L1→B
: If {B}
: {B+1}+sign{B+3}→{B+1}
: {B+2}+sign{B+4}→{B+2}
: If {B+1}<96 and ({B+2}<64)
:
: Else
:
: End
: End
:End
What do we want to do depending on the result of this test ? Simple enough : if the bullet is on-screen, display it with a Pt-On, else simply make it inactive by setting its first byte to 0, so it will be ignored on the next game loop iteration.
Code:
:For(A,0,19)
: A*5+L1→B
: If {B}
: {B+1}+sign{B+3}→{B+1} ← note the use of sign{ : we must handle negative speeds as well
: {B+2}+sign{B+4}→{B+2}
: If {B+1}<96 and ({B+2}<64)
: Pt-On({B+1},{B+2},[40A0A0A0E0000000]) ← nice little bullet sprite
: Else
: 0→{B}
: End
: End
:End
Now, we have the bullet handling code done. Yeah, that was really all ! But hang on a second, the whole program is not done yet. We still have to make the ship shoot, or we'll just stay with a movable ship and a bullet handler that handles nothing.
So what we want to do is : each time the user presses [2nd], one bullet is fired from the ship. The bullet must go up (Y speed will be -2) and at the left or right (X speed will be either -1, 0 or 1, chosen randomly). To do so, we must keep track of which bullet/entry has been used in the table, and which are free.
So we'll use another variable which will vary between 0 and 19 inclusive and which will be the current entry in the bullet table - the bullet that can be shot. Each time we will shoot, we will increment that variable, and when it reaches 20, we set it back to 0 (because we can shoot 20 bullets). We will use that variable as an index in the bullet table. What we also want is a timer, to prevent the player to shoot too fast, because then the game becomes too easy. Each time we shoot, we set that timer to 8, and only shoot if that timer is 0, so that the player can only shoot every 8 frames.
First, we must set our variables to 0 (outside the game loop). Let's use C as the bullet counter and T as the timer.
Code:
:0→C→T
Now, we can write our shooting code. Add it in the game loop, just before the "EndIf getKey(15)" line.
Code:
:If getKey(54) ← the code for the [2nd] key
: !If T
: C*5+L1→B ← select the correct bullet in the bullet table
: 1→{B} ← make the bullet active !
: X+2→{B+1} ← make the bullet start at the top-center of the ship sprite
: Y→{B+2}
: rand^3-1→{B+3} ← an X speed of either -1, 0 or 1 (rand^3 returns either 0, 1 or 2).
: -2→{B+4} ← the bullet will always go up
: C+1^20→C ← increment C and set it back to 0 if it is 20 using modulo
: 8→T
: Else
: T--
: End
:End
And now we're done ! Yeah, already ! Here's the full code :
Code:
:.SHMUPSIM
:[1028448200000000]→Pic1
:0→C→T→X→Y
:
:Fill(L1,100,0)
:
:While 1
:
:For(A,0,19)
:A*5+L1→B
:If {B}
:{B+1}+sign{B+3}→{B+1}
:{B+2}+sign{B+4}→{B+2}
:If {B+1}<96 and ({B+2}<64)
:Pt-On({B+1},{B+2},[40A0A0A0E0000000])
:Else
:0→{B}
:End
:End
:End
:
:Pt-On(X,Y,Pic1)
:
:DispGraphClrDraw
:getKey(3)-getKey(2)+X→X
:getKey(1)-getKey(4)+Y→Y
:
:If getKey(54)
:!If T
:C*5+L1→B
:1→{B}
:X+2→{B+1}
:Y→{B+2}
:rand^3-1→{B+3}
:-2→{B+4}
:C+1^20→C
:8→T
:Else
:T--
:End
:End
:
:EndIf getKey(15)
If you try the code, you'll see that this is going a little too fast, so I put a Pause 20 in that screenshot, right after the DispGraphClrDraw command :
Have fun with that technique, and don't hesitate to ask questions if you don't understand something
EDIT : fixed the issue Charty pointed out