-
Notifications
You must be signed in to change notification settings - Fork 4
/
SpecBong.asm
335 lines (304 loc) · 16.5 KB
/
SpecBong.asm
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
328
329
330
331
332
333
334
335
;-------------------------------
; SpecBong - tutorial-like project to load Layer2 image and move sprites
; © Peter Helcmanovsky, John McGibbitts 2020, license: https://opensource.org/licenses/MIT
;
; to build this ASM file we use https://github.com/z00m128/sjasmplus command:
; sjasmplus --fullpath --nologo --lst --lstlab --msg=war SpecBong.asm
; (this will also produce the listing file, so we can review the machine code generated
; and addresses assigned to various symbols)
;
; to convert BMP to upside-down TGA I use ImageMagick "convert" command:
; convert SpecBong.bmp -flip tga:SpecBong.tga
; (the upside down uncompressed 8bpp TGA has the advantage that it can be just binary
; included as L2 pixel data, from correct offset, no need of any further conversion)
; adjusting sjasmplus syntax to my taste (a bit more strict than default) + enable Z80N
OPT --syntax=abfw --zxnext
OPT --zxnext=cspect ;DEBUG enable break/exit fake instructions of CSpect (remove for real board)
STRUCT S_SPRITE_4B_ATTR ; helper structure to work with 4B sprites attributes
x BYTE 0 ; X0:7
y BYTE 0 ; Y0:7
mrx8 BYTE 0 ; PPPP Mx My Rt X8 (pal offset, mirrors, rotation, X8)
vpat BYTE 0 ; V 0 NNNNNN (visible, 5B type=off, pattern number 0..63)
ENDS
; selecting "Next" as virtual device in assembler, which allows me to set up all banks
; of Next (0..223 8kiB pages = 1.75MiB of memory) and page-in virtual memory
; with SLOT/PAGE/MMU directives as needed, to assemble code/data to different parts
; of memory
DEVICE ZXSPECTRUMNEXT
; the default mapping of memory is 16k banks: 7, 5, 2, 0 (8k pages: 14,15,10,11,4,5,0,1)
; ^ it's the default mapping of assembler at assembling time, at runtime the NEXLOAD
; will set the default mapping the same way, but first 16k is ROM, not bank 7.
; $8000..BFFF is here Bank 2 (pages 4 and 5) -> we will put **all** code here
ORG $8000
start:
; break at start when running in CSpect with "-brk" option
break : nop : nop ; 2x"nop" after "break" to make real board survive "break" (= ld bc,0)
; disable interrupts, we will avoid using them to keep code simpler to understand
di
; make the Layer 2 visible and reset some registers (should be reset by NEXLOAD, but to be safe)
nextreg $69,$80 ; Layer 2 visible, ULA bank 5, Timex mode 0
nextreg $15,$01 ; LoRes off, layer priority SLU, sprites visible
nextreg $12,9 ; visible Layer 2 starts at bank 9
nextreg $70,0 ; 256x192x8 Layer 2 mode, L2 palette offset +0
nextreg $16,0 ; Layer 2 X,Y offset = [0,0]
nextreg $71,0 ; including the new NextReg 0x71 for cores 3.0.6+
nextreg $17,0
; setup Layer 2 palette - map palette data to $E000 region, to process them
nextreg $57,$$BackGroundPalette ; map the memory with palette to the $E000..$FFFF
; the "$$" is special operator of sjasmplus to get memory page of particular
; label (the 8kiB memory page)
nextreg $43,%0'001'0'0'0'0 ; write to Layer 2 palette, select first palettes
nextreg $40,0 ; color index
ld b,0 ; 256 colors (loop counter)
ld hl,BackGroundPalette ; address of first byte of 256x 24 bit color def.
; calculate 9bit color from 24bit value for every color
; -> will produce pair of bytes -> write that to nextreg $44
SetPaletteLoop:
; TGA palette data are three bytes per color, [B,G,R] order in memory
; first byte to calculate: RRR'GGG'BB
ld a,(hl) ; Blue
inc hl
rlca
rlca
ld c,a ; preserve blue third bit in C.b7 ($80)
and %000'000'11 ; two blue bits at their position
ld e,a ; preserve blue bits in E
ld a,(hl) ; Green
inc hl
rrca
rrca
rrca
and %000'111'00
ld d,a ; preserve green bits in D
ld a,(hl) ; Red
inc hl
and %111'000'00 ; top three red bits
or d ; add green bits
or e ; add blue bits
nextreg $44,a ; RRR'GGG'BB
; second byte is: p000'000B (priority will be 0 in this app)
xor a
rl c ; move top bit from C to bottom bit in A (Blue third bit)
rla
nextreg $44,a ; p000'000B p=0 in this image always
djnz SetPaletteLoop
; the image pixel data are already in the correct banks 9,10,11 - loaded by NEX loader
; nothing to do with the pixel data - we are done
; ------------------------------------------------------------------------------------
; Part 2 - uploading sprite graphics and debug-display of all gfx patterns in 8x8 grid
; (the debug display does already use HW sprites to show all gfx patterns)
; ------------------------------------------------------------------------------------
; sprite gfx does use the default palette: color[i] = convert8bitColorTo9bit(i);
; which is set by the NEX loader in the first sprite palette
; nothing to do here in the code with sprite palette
; upload the sprite gfx patterns to patterns memory (from regular memory - loaded by NEX loader)
; preconfigure the Next for uploading patterns from slot 0
ld bc,$303B
xor a
out (c),a ; select slot 0 for patterns (selects also index 0 for attributes)
; we will map full 16kiB to memory region $C000..$FFFF (to pages 25,26 with sprite pixels)
nextreg $56,$$SpritePixelData ; C000..DFFF <- 8k page 25
nextreg $57,$$SpritePixelData+1 ; E000..FFFF <- 8k page 26
ld hl,SpritePixelData ; HL = $C000 (beginning of the sprite pixels)
ld bc,$5B ; sprite pattern-upload I/O port, B=0 (inner loop counter)
ld a,64 ; 64 patterns (outer loop counter), each pattern is 256 bytes long
UploadSpritePatternsLoop:
; upload 256 bytes of pattern data (otir increments HL and decrements B until zero)
otir ; B=0 ahead, so otir will repeat 256x ("dec b" wraps 0 to 255)
; you can use F7 in CSpect debugger to single step over each iteration to see how
; the B wraps from initial 00 to FF, FE, ... until 00 again, doing 256x `outi`
; or you can use F8 to do the whole `otir` in single step-over way
dec a
jr nz,UploadSpritePatternsLoop ; do 64 patterns
; ------------------------------------------------------------------------------------
; Part 3 - creating base main-loop, moving few sprites around, no controls yet
; ------------------------------------------------------------------------------------
; init 32 snowballs and one player - the in-memory copy of sprite attributes
; init them at some debug positions, they will for part 3 just fly around mindlessly
ld ix,SprSnowballs ; IX = address of first snowball sprite
ld b,32 ; define 32 of them
ld hl,0 ; HL will generate X positions
ld e,32 ; E will generate Y positions
ld d,$80 + 52 ; visible sprite + snowball pattern (52, second is 53)
InitBallsLoop:
; set current ball data
ld (ix+S_SPRITE_4B_ATTR.x),l
ld (ix+S_SPRITE_4B_ATTR.y),e
ld (ix+S_SPRITE_4B_ATTR.mrx8),h ; clear pal offset, mirrors, rotate, set x8
ld (ix+S_SPRITE_4B_ATTR.vpat),d
; adjust initial position and pattern for next ball
add hl,13 ; 13*32 = 416: will produce X coordinates 0..511 range only
ld a,e
add a,5
ld e,a ; 5*32 = 160 pixel spread vertically
ld a,d
xor 1 ; alternate snowball patterns between 52/53
ld d,a
; advance IX to point to next snowball
push de
ld de,S_SPRITE_4B_ATTR
add ix,de
pop de
djnz InitBallsLoop
; init player at debug position
ld ix,SprPlayer
ld (ix+S_SPRITE_4B_ATTR.x),32+16 ; near left of paper area
ld (ix+S_SPRITE_4B_ATTR.y),206 ; near bottom of paper area
ld (ix+S_SPRITE_4B_ATTR.mrx8),0 ; clear pal offset, mirrors, rotate, x8
ld (ix+S_SPRITE_4B_ATTR.vpat),$80 + 2 ; pattern "2" (player
; main loop of the game - first version for Part 3
; (will have many shortcoming, to be resolved in later parts of tutorial)
GameLoop:
; wait for scanline 192, so the update of sprites happens outside of visible area
; this will also force the GameLoop to tick at "per frame" speed 50 or 60 FPS
call WaitForScanlineUnderUla
; upload sprite data from memory array to the actual HW sprite engine
; reset sprite index for upload
ld bc,$303B
xor a
out (c),a ; select slot 0 for sprite attributes
ld hl,Sprites
ld bc,$57 ; B = 0 (repeat 256x), C = sprite pattern-upload I/O port
; out 512 bytes in total (whole sprites buffer)
otir
otir
; adjust sprite attributes in memory pointlessly (in debug way) just to see some movement
call SnowballsAI
; do the GameLoop infinitely
jr GameLoop
;-------------------------------------------------------------------------------------
; "AI" subroutines
SnowballsAI:
ld ix,SprSnowballs
ld de,S_SPRITE_4B_ATTR
ld b,32
.loop:
; HL = current X coordinate (9 bit)
ld l,(ix+S_SPRITE_4B_ATTR.x)
ld h,(ix+S_SPRITE_4B_ATTR.mrx8)
; adjust it by some +- value deducted from B (32..1 index)
ld c,0 ; mirrorX flag = 0
ld a,b
and 3 ; A = 0,1,2,3
sli a ; A = 1, 3, 5, 7
sub 4 ; A = -3, -1, +1, +3
; do: HL += signed(A) (the "add hl,a" is "unsigned", so extra jump+adjust needed)
jr nc,.moveRight
dec h
ld c,$08 ; mirrorX flag = 1
.moveRight:
add hl,a
; put H and C together to work as palette_offset/mirror/rotate bits with X8 bit
ld a,h
and 1 ; keep only "x8" bit
or c ; add desired mirrorX bit
; store the new X coordinate and mirror/rotation flags
ld (ix+S_SPRITE_4B_ATTR.x),l
ld (ix+S_SPRITE_4B_ATTR.mrx8),a
; alternate pattern between 52 and 53 - every 8th frame (in 50Hz: 6.25FPS=160ms)
; the 8th frame check is against B (counter), so not all sprites update at same frame
ld a,(TotalFrames)
xor b
and 7
jr nz,.notEightFrameYet
ld a,(ix+S_SPRITE_4B_ATTR.vpat)
xor 1
ld (ix+S_SPRITE_4B_ATTR.vpat),a
.notEightFrameYet:
add ix,de ; next snowball
djnz .loop ; do 32 of them
ret
; ------------------------------------------------------------------------------------
; Part 4 - control of game loop speed and animation speed of snowballs
; ------------------------------------------------------------------------------------
WaitForScanlineUnderUla:
; because I decided early to not use interrupts to keep the code a bit simpler
; (simpler to follow in mind, not having to expect any interrupt everywhere)
; we will be syncing the main game loop by waiting for particular scanline
; (just under the ULA paper area, i.e. scanline 192)
; update the TotalFrames counter by +1
ld hl,(TotalFrames)
inc hl
ld (TotalFrames),hl
; if HL=0, increment upper 16bit too
ld a,h
or l
jr nz,.totalFramesUpdated
ld hl,(TotalFrames+2)
inc hl
ld (TotalFrames+2),hl
.totalFramesUpdated:
; read NextReg $1F - LSB of current raster line
ld bc,$243B
ld a,$1F
out (c),a ; select NextReg $1F
inc b
; if already at scanline 192, then wait extra whole frame (for super-fast game loops)
.cantStartAt192:
in a,(c) ; read the raster line LSB
cp 192
jr z,.cantStartAt192
; if not yet at scanline 192, wait for it ... wait for it ...
.waitLoop:
in a,(c) ; read the raster line LSB
cp 192
jr nz,.waitLoop
; and because the max scanline number is between 260..319 (depends on video mode),
; I don't need to read MSB. 256+192 = 448 -> such scanline is not part of any mode.
ret
;-------------------------------------------------------------------------------------
; data area
TotalFrames: ; count frames for purposes of slower animations/etc
DD 0
; reserve full 128 sprites 4B type (this demo will not use 5B type sprites)
ALIGN 256 ; aligned at 256B boundary w/o particular reason (yet)
Sprites:
DS 128 * S_SPRITE_4B_ATTR, 0
; "S_SPRITE_4B_ATTR" works as "sizeof(STRUCT), in this case it equals to 4
; the later sprites are drawn above the earlier, so for Part 3 the sprites
; 0..31 will be used for snowballs, and sprite 32 for player
; adding symbols to point inside the memory reserved above
SprSnowballs: EQU Sprites + 0*S_SPRITE_4B_ATTR ; first snowball sprite at this address
SprPlayer: EQU Sprites + 32*S_SPRITE_4B_ATTR ; player sprite is here
;-------------------------------------------------------------------------------------
; reserve area for stack at $B800..$BFFF region
ORG $B800
DS $0800-2, $AA ; $AA is just debug filler of stack area
initialStackTop:
DW $AAAA
;-------------------------------------------------------------------------------------
; game data in different (later) banks, pre-loaded in sjasm virtual device memory
; so they get stored in the NEX file, and loaded by NEX loader to desired bank
;-------------------------------------------------------------------------------------
; pre-load the image pixel data from TGA file into memory (to store it in NEX file)
; the pixel data will be in 16k banks 9, 10, 11 (8k pages: 18, 19, .., 23)
; We will use the last page region $E000..$FFFF to map through all the pages and
; include the binary pixel data from the TGA file, using sjasmplus MMU directive
; map into last slot first Layer2 page (8ki page number = 16ki bank * 2 = 9*2 = 18)
MMU 7 n, 9*2 ; slot 7 = $E000..$FFFF, "n" option to auto-wrap into next page
; now include the binary pixel data from the TGA file at the $E000 address
ORG $E000
INCBIN "SpecBong.tga", 0x12 + 3*256, 256*192
; the assembler will automatically wrap around the $E000 with next 8k page
; until the requested amount of bytes is included, setting up pages 18..23
; palette of image (will land to page 24, first free byte after pixel data)
; verify the assumption that the palette starts where expected (page 24, $E000)
ASSERT $ == $E000 && $$ == 24
BackGroundPalette:
INCBIN "SpecBong.tga", 0x12, 3*256 ; 768 bytes of palette data
; sprite pixel data from the raw binary file SBsprite.spr, aligned to next
; page after palette data (8k page 25), it will occupy two pages: 25, 26
MMU 6 7, $$BackGroundPalette + 1 ; using 16ki memory region $C000..$FFFF
ORG $C000
SpritePixelData:
INCBIN "SBsprite.spr"
CSPECTMAP "SpecBong.map" ; map file for #CSpect
; to use it - add to CSpect.exe command line: -map=SpecBong.map
;-------------------------------------------------------------------------------------
; all the data are in the virtual-device memory, now dump it into NEX file
SAVENEX OPEN "SpecBong.nex", start, initialStackTop, 0, 2 ; V1.2 enforced
SAVENEX CORE 3, 0, 0 ; core 3.0.0 required
SAVENEX CFG 1 ; blue border (as debug)
SAVENEX AUTO ; dump all modified banks into NEX file
; currently the 16k Banks stored will be: 2, 9, 10, 11, 12, 13
SAVENEX CLOSE