-
Notifications
You must be signed in to change notification settings - Fork 0
/
readme.txt
327 lines (243 loc) · 10.9 KB
/
readme.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
NextBASIC Shmup!
================
Kind of an attempt to do a classical style vertical shoot 'em up game, using new
features of NextBASIC. Born as a demo for me to learn the language, it grew up a
lot, and now we will see if it ends up being an actual game :)
How to load
-----------
1.- Copy the contents of this repo to your Next SD card.
2.- In your Next, go to its folder and type:
.txt2bas shmup.bas.txt
3.- Load the newly created 'shmup.bas' and RUN it.
About emulators
---------------
Current version of CSpect (2.15.1) is not able to run the game. The previous
version (2.14.8) does run it but very slowly, about 50-75% of the speed on
actual hardware.
Motivation for this project
===========================
I wanted to do a game that achieves 2 things:
(1) Uses NextBASIC capabilities exclusively, so no calls to assembly, not even
an audio driver is used, just to test the capabilities of the language on its
own.
(2) Has "Dijkstra-compliant" code, with no GO TOs, and hopefully no line
numbers. This is difficult since makes me give up on GO SUBs and the
possibility to use 'computed GOSUBS' as "dictionaries", avoiding a good deal of
IF-THENs and improving performance. But I wanted to make readable code with a
proper representation of "modular programming", so if you read every PROC on
its own you should be able to understand everything.
I know I betrayed (2) on the setInput() procedure, but will be the only
exception ;)
From here on, I'll explain every trick the program implements.
Player movement
---------------
Movement of the player ship is not controlled "manually", there is no use of
variables for its position. Instead it uses a "finite-state machine" (FSM)
where the ship can be in 1 of 9 different states, and transition between states
occur by player input. Each state mean a different "automatic movement" type
for the ship, through SPRITE CONTINUE statement, which automagically takes care
of player position and limits of movement.
For info on this and other sprite manipulation commands, see
NextBASIC_New_Commands_and_Features.pdf in NextZXOS documentation, pages 7-11.
Direct link:
https://gitlab.com/thesmog358/tbblue/-/blob/master/docs/nextzxos
/NextBASIC_New_Commands_and_Features.pdf
FSM states
----------
State Use
0 Standing still (neutral/no input)
1 Moving Right
2 Moving Left
3 Moving Up
4 Moving Down
5 Diagonal Up/Right
6 Diagonal Down/Right
7 Diagonal Up/Left
8 Diagonal Down/Left
9 Limbo (player dead, used to disable input)
Sound buffering
---------------
Sound was though out as a "circular queue", where %a() is the queue, %a is the
"head" or "front", which points to the next tone to play, and %u is the "tail"
or "rear" of the data, which is the last tone included in the queue.
Being in a circular queue, %a and %u only advance in one direction, and once
they reach the end, they go back to the beginning.
%a %u
| |
v ]
*-----------------------------------------------------------------* <- %a()
^ |
|_________________________________________________________________|
Any action that requires a sound fills the queue, advancing the "tail", and in
each SPRITE MOVE the "head" reads and plays the buffered sound, advancing to
the next position. If the "head" and the "tail" are in the same position,
nothing is played nor advanced (empty queue).
Note that there's no handling of "queue size", so no error checking if it's
completely "filled", it just start to overwrite old tones. In other words,
doing a full "turn" with the "tail" without making the "head" advance to catch
up will make the previously buffered sounds disappear.
%b(), %b and %v comprise a second queue, for a second audio channel. There are
other queues: coarse tone, %c(), and noise, %n(). These use the same head and
tail as the second channel queue, %b and %v, because they play through that
channel.
Sound playing
-------------
Only one AY is used, with "2 channels", since A and C play the same sound, and
their coarse setting is always 0. On the other side, B has coarse and fine, and
is able to produce noise when necessary.
Then, 4 elements are needed:
1.- Channel A & C fine tone - %a() - %a - %u
2.- Channel B fine tone - %b() - %b - %v
3.- Channel B coarse tone - %c() - %b - %v
4.- Noise (on or off) - %n() - %b - %v
As said in the previous section, 4 circular queues are used, with the last 3
using the same "head" and "tail", %b and %v respectively.
To avoid changing the PSG mixer settings every time, previous mixer settings
are stored in %m, as a binary value with 0 = enabled and 1 = disabled:
0 0 0 N 0 A B A
...with N = noise
B = B channel
A = A & C channels
Then, %m = (16*N) + (5*A) + (2*B)
If you are wondering what is all of this, why it works this way, please read my
guide about generating sounds through OUT commands to the PSG:
https://www.specnext.com/forum/viewtopic.php?f=14&t=1796
At every SPRITE MOVE, queues are read, and from that the channels needed are
identified. If the enabled channels are different from what was stored in the
previous SPRITE MOVE, mixer settings are changed in the PSG and saved to %m.
Only after that, queues will advance and actual sound will be played.
Enemy wave spawning
-------------------
An "enemy wave" in this game's context is "enemies that spawn at the exact same
time". Having a short time between waves provides the illusion of a "coordinated
attack", while a longer time gives the pause one would expect from a "different
group of attackers".
DATA about enemies are READ by the wave() procedure in the following format:
- Number of enemies in the wave
- Time until next wave (in "game frames" - amount of SPRITE MOVE calls)
- Times the number of enemies, the following structure:
- Enemy type
- X position of spawn
- Y position of spawn
- "Null terminator" (0).
"Enemy type" in this context is "how the enemy moves across the screen".
Position of spawn should always be outside of visible screen (BORDER coords).
RESTORE, which means spawning reset to the beginning, will occur when:
- "Number of enemies" and "Next spawn" are both 0
- "Null terminator" is not found when expected
Any of them could mean "end of stage" in the future. We'll see.
E.g. the first DATA in the program (near the end, after setInput() procedure)
contains the following:
2, 40, 1, 130, 0, 1, 170, 0, 0
This indicates that this wave is composed of 2 enemies, and the next wave after
this will appear after 40 'SPRITE MOVEs'.
Then, in this wave the first enemy is type 1, and will appear at x=130, y=0.
The second enemy is also type 1 and will appear at x=170, y=0.
Then, the needed "0" to finish the reading of this wave.
Type 1 enemies descend vertically. This is why the first enemies you see on
screen are a couple at the top center, descending. You can check the
different 'types' at the "spawn()" procedure.
Boss
----
Currently, only 1 is allowed to exist at any time. It's composed of 4 unified
sprites with the following structure:
2 3
0 1
As sprites 1-3 have a pattern relative to the anchor sprite 0, a single SPRITE
CONTINUE to this anchor can be used for animation, since the relative sprites
inherit the pattern from the anchor, or rather, the sum of their original
pattern plus the anchor's pattern. This explains the order in which the sprites
are in the file 'shmup.spr', since adding numbers close to 0 provides control
on what the final patterns for every relative sprite will be.
Again, see NextBASIC_New_Commands_and_Features.pdf, pages 7 to 10
Variables
---------
Some are local for the procedure they appear in; they are shown here because of
their importance.
Name Type Use
%s int current State in the FSM
%i int current directional Input, FSM format
%c int player fire Cooldown (time before able to fire again)
%k() int array player controls (Keys or Kempston)
%l int Layer 2 position, for scroll effect
%f int movement Frames to wait before scroll
%m int current PSG Mixer settings
%w int needed mixer settings (to compare with %m)
%a() int array circular queue for A&C channels' sounds
%a int "head" of A queue
%u int "tail" of A queue
%b() int array circular queue for B channel's fine sounds
%c() int array circular queue for B channel's Coarse sounds
%n() int array circular queue for Noise on/off (B channel)
%b int "head" of B queues
%v int "tail" of B queues
%p() int array Player state (see below)
%d int player truly Dead (go to game over)
%e int time until next Enemy wave
%h int Hyperaggresiveness? Time until next shot from enemy
%o() int array bOss state (see below)
Player state
------------
%p() is an array with info about the player:
Index Use
0 Dead (0), alive (1) or invulnerable (241)*
1 Lives
2 Score
3 Timer to respawn (used when dead)
Also used to disable invulnerability after respawn
4 Cycles of enemy waves completed
(times where DATA of enemy waves have been fully READ)
* Index 0, player vital status, affects player SPRITE flags, so colors are
inverted when player is invulnerable (just respawning).
1 = 00000001 <- visible SPRITE, normal palette
241 = 11110001 <- visible SPRITE, palette offset changed
+ 8 = 00001000 <- + X-mirrored, when going left
Boss state
----------
Likewise, %o() is an array with info about the boss:
Index Use
0 Absent (0), type of movement (1-3)
1 Health
2 Time until next shot *
* The boss only shoots when its type of movement is 3
Sprite IDs
----------
Id Use
48 Player - couldn't be any other id :)
49-57 Player shots
4-30 Enemies
31-47 Enemies' shots
60-77 Enemies' explosions/deaths
127 Player's death
0-3 Boss
126 Boss' death
Sprite patterns
---------------
Pattern Use
4 Player ship, straight
5 Player ship, leaning right
16 Player shot
6-11 Explosion animation
12-15 Enemies and their animations
0-3 Half of boss character and animation
17 Enemy shot
18 Boss shot (to do)
To Do
-----
Have to make it playable!
- Enemies shooting in different directions
- Make boss to shoot
- Other types of waves
- Powerups?
- Improved ending? Change its condition?
Acknowledgments
===============
The entire Next Team, for the wonderful machine
Garry Lancaster, for the language and the Invaders game which inspired this
Remy Sharp, for the info about audio and Layer 2
Simon N Goodwin and Matthew Neale, for performance tips
Rodrigo Moreira, for the beautiful sprites :)
You, for your interest in the game and getting this far in the document
Jaime Moreira
2020-08-23
Last updated: 2022-03-20