Games - Marsmare game device and self-written tools: assembler, sprite editor, map editor, IDE

Marsmare: Alienation
 At the competition  Yandex Retro Games Battle 
platformerMarsmare: took first place 
Alienation from the team Drunken Fly (beginners  
on the Spectrum - from2019!). We contacted 
with the author of the code for this game -Nikolai Zapo─ 
flax. 

 * * *

Alone> 
Looked through the codeMarsmare: Alienationin de─ 
bagger and found a very unusual one there 
rendering method. What are the approximate characteristics─ 
sticks - how many graphics it supports, 
how many sprites are on the screen at the same time 
is there clipping (if so,then how 
does it work)? 

Nikolai Zapolnov> 
 I was initially guided by the principle
described in this article: 
http://oldmachinery.blogspot.com/2014/04/ 
zx-sprites.html 
   Sprites are preshifted (and eat up a lot of memory
I must say, that’s why they had to be heavily
compress).
   The rendering is based on the address table (opi─
shu below principle; separate table for each
I set the width of the sprite, according to familiarity: 
DrawRoutines8, DrawRoutines16 and 24), for example─ 
measures:

DrawRoutines8: 
dw DirtyBits8 
4 times: dw Skip8 7 times, SkipEnd8 
3 times:dw Draw87 times,DrawEnd8 
        dw Draw8 7 times, DrawBound8 
7 раз:  dw Draw8 7 раз, DrawEnd8 
        dw Draw8 7 раз, DrawBound8 
7 раз:  dw Draw8 7 раз, DrawEnd8 
        dw Draw8 7 раз, DrawBound8 
24 раза dw DrawSprite@@done 

Пропускает одну строку (не кратную 8): 
Skip8: 
       inc hl
       inc hl
       inc d                  ;Y0-Y2 += 1
       ret

Пропускает одну строку (кратную 8): 
SkipEnd8: 
       inc hl
       inc hl
       ex de,hl
       add hl,bc ;BC = 0xf920 ;Y0-Y2 -= 7
  ;Y3-Y5 += 1
 ex de,hl
ret

Draws one line and moves on to the next
line (not a multiple of 8): 
Draw8: 
 ld a,(de)
 and (hl)
 inc hl
 or (hl)
 inc hl
 ld (de),a
 inc d  ;Y0-Y2 += 1
ret

Draws one line and moves on to the next
line (multiples of 8): 
DrawEnd8: 
 ld a,(de)
 and (hl)
 inc hl
 or (hl)
 inc hl
 ld (de),a
 ex de,hl
 add hl,bc ;BC = 0xf920 ;Y0-Y2 -= 7
  ;Y3-Y5 += 1
 ex de,hl
ret

Draws one line and moves on to the next
line (at the boundary where the screen address is
Spectrum jumps): 
DrawBound8: 
 ld a,(de)
 and (hl)
 inc hl
 or (hl)
 inc hl
 ld (de),a
 inc d ;before:Y0-Y2 = 7
  ;after: Y0-Y2 = 0,Y6-Y7 += 1
 ld a,e ;Y3-Y5 = 7
 add a,c ;0x20
 ld e,a ;Y3-Y5 = 0
ret

   Routine for drawing a sprite:
  1) puts SP to a row in the table, respectively─
corresponding to the starting coordinateY;
 2) replaces in a row in the table for
last coordinateYaddress to 
DrawSprite@@done  (preserving the previous value─nie)
 3) does RET.
Each rendering routine does RET and
jumps to the next one until it reaches 
DrawSprite@@done. They are arranged in the table─ 
we need to jump over correctly
row addresses.
   Upon returning toDrawSprite@@doneres─
set the row in the table to initial
meaning.
   There is small clipping vertically behind
account  Skip8, Skip8End  and DrawSprite@@done.
Sprite lines that go to the top─
The coordinates are not drawn.  And the lines
sprites that fall on the lower coor─
dinata, forcefully stop drawing
sprite. But if you set it too high
coordinate Y, then we will miss the table
and there will be a crash.
   There is no horizontal clipping.
   For different sprite sizes, add─
There is a small descriptor sign:

SprOff16x16_8: 
 dw 0
 repeat 7,Y
 dw (16*4 + 2) + (16*6 + 2) * Y
endrepeat
 db 16*2 ;height in pixels * 2
 db 2,1,2 ;num of dirty vert tiles
 ;(default, align16, lessEq8)
 dw DrawSprite@@exitSP

   The last word is the address of the procedure
Rendering solutions. There may still be 
DrawSprite@@coloredXX for color sprites 
(pills, cartridges, crystals, fuel),
it writes additional attributes. 
DrawSprite@@exitSP - normal output, like this 
for most sprites.
 Num of dirty vert tiles  - quantity
tiles (tiles - 16x16), which gets dirty
sprite (they will be redrawn next
frame to color the sprite in front of the rice─
what a new frame). Three numbers instead of one
for optimization: for the general case (not op─
optimized), for the case when Y is multiple 
16, and when (Y & 15) <= 8. 
   The first 8 words are offsets from the beginning
sprite for each offset by X. Sprite
looks like this (the offsets just indicate
to the corresponding dw DrawRoutinesXX ):

dw SprOff16x16_8 
dw DrawRoutines16 
db 0xe1.0x00.0x87.0x00 ; ...____..____... 
db 0xc1,0x0C,0x07,0x30 ; ..__##_.__##_... 
db 0xc0.0x10.0x07.0x40 ; .._#_____#___... 
db 0xc0,0x0F,0x1f,0x80 ; ..__#####__..... 
db 0x80,0x10,0x0f,0x40 ; .__#_____#__.... 
db 0x80.0x20.0x07.0xA0 ; ._#_____#_#__... 
db 0x80,0x2C,0x07,0x50 ; ._#_##___#_#_... 
db 0x80,0x26,0x03,0x10 ; ._#__##____#__.. 
db 0x80,0x06,0xc3,0x08 ; .____##_..__#_.. 
db 0xc0,0x16,0xe1,0x08 ; .._#_##_..._#__. 
db 0xc0,0x0C,0xe1,0x04 ; ..__##__...__#_. 
db 0xc1,0x10,0xf1,0x04 ; .._#___....._#_. 
db 0xc0,0x08,0xf1,0x04 ; ..__#___...._#_. 
db 0xe0,0x06,0x01,0x04 ; ...__##______#_. 
db 0xf0,0x01,0x01,0xF8 ; ....___######__. 
db 0xfc,0x00,0x03,0x00 ; ......________.. 
dw DrawRoutines24 
db 0xf0,0x00,0xc3,0x00,0xff,0x00 
db 0xe0,0x06,0x83,0x18,0xff,0x00 
db 0xe0,0x08,0x03,0x20,0xff,0x00 
db 0xe0,0x07,0x0f,0xC0,0xff,0x00 
db 0xc0,0x08,0x07,0x20,0xff,0x00 
db 0xc0,0x10,0x03,0x50,0xff,0x00 
db 0xc0,0x16,0x03,0x28,0xff,0x00 
db 0xc0,0x13,0x01,0x08,0xff,0x00 
db 0xc0,0x03,0x61,0x04,0xff,0x00 
db 0xe0,0x0B,0x70,0x04,0xff,0x00 
db 0xe0,0x06,0x70,0x02,0xff,0x00 
db 0xe0,0x08,0xf8,0x02,0xff,0x00 
db 0xe0,0x04,0x78,0x02,0xff,0x00 
db 0xf0,0x03,0x00,0x02,0xff,0x00 
db 0xf8,0x00,0x00,0xFC,0xff,0x00 
db 0xfe,0x00,0x01,0x00,0xff,0x00 
...ещё 6 блоков с DrawRoutines24... 
       (все сдвиги по X)

   В спрайте - AND-маска и OR-маска.
   Количество  одновременных  спрайтов  на
экране  сильноdepends on sprite size
and the number of stained squares (for example─
mer, sprite8x8,standing between two tiles─
we will eat more resources than we harvest─
placed on the border of a familiar place, as necessary
erase more tiles).
   Overall, that's 1 player sprite + 3
large enemies ( 16x24, with multiple  8
coordinates X - 24x24 ) and several sprays─
com-shots ( 8x8 ). On the maps, where else
were there animated tiles or other sprites─
you (elevator, airlock, etc.), set it smaller
enemies.

   Tiles are drawn by separate routines.All
screen - tile map  16x10  (for the current
cards are stored words - direct addresses tai─
catch in memory so that when drawing it does not dissipate
read addresses; the card data itself is stored
one byte to save space). Add─
there is a passability bit mask and two
masks of dirty tiles (one per regular
and shadow screen buffer).
   Tile data takes36bytes (32ba─
yta pixels and 4 bytes attributes) and go to
in alternating order:
 1 -> 2 |
 4 <- 3 v
 This order allows you to save money─
leg of clocks on updating addresses in the regi─
fear.

Alone> 
As I understand it, sprites should be located in 
lower memory, because the output alternates ─ 
in the lower and upper screen? 

Nikolai Zapolnov> 
 Yes, in the lower memory (banks 2 and 5), or
in the bank 7 (where the shadow screen is located).
I really miss, of course, the possibility of ma─
drink memory on ROM.

Alone> 
 Pills, cartridges, etc. made not tiles─ 
so that they can be superimposed on any background? 

Nikolai Zapolnov> 
 So that they can overlap the background and
to make it easy to implementopportunity
select them. In addition, initially we were ─
we laid them with regular sprites and painted them
decided later (they were hard to see), but
add the ability to draw atri─
bottles together with the sprite looked the best
solution than to rewrite everything for use─
changing tiles.

Alone> 
 How is the tile animation done? For those─ 
Tabernacle location creates a list of changeable 
tiles, and then do everything manually? How are these descriptions─ 
is it in the editor? 

Nikolai Zapolnov> 
 This is not described in any way in the editor,
animations are set for specific tiles in
lua script (as well as delays for animations
tiles).
   A list is generated for each card
animated tiles:

db 3 ;numAnimatables 
;animatable 1 
db 1.0 ;delay,counter 
db 2.0 ;count,index 
dw MapAnim_358e04958311f5a6b5f9 ;tile list 
dw Map_05_08@@anim1 - Map_05_08 ;offset 
                            ;into map data 
db 12,4,0 ;mapY,mask1,mask2 
dw 420 ;target screen address 
;animatable 2 
db 4,0 ;delay,counter 
db 8,0 ;count,index 
dw MapAnim_6aee9a687735e6fe7e67 ;tile list 
dw Map_05_08@@anim2 - Map_05_08 ;offset 
                           ;into map data 
db 12,16,0 ;mapY,mask1,mask2 
dw 424 ;target screen address 
;animatable 3 
db 4,0 ;delay,counter 
db 8,0 ;count,index 
dw MapAnim_6aee9a687735e6fe7e67 ;tile list 
dw Map_05_08@@anim3 - Map_05_08 ;offset 
                            ;into map data 
db 12,32,0 ;mapY,mask1,mask2 
dw 426 ;target screen address 
    ... 
MapAnim_6aee9a687735e6fe7e67: 
dw MapTile176 * 36 + TILES_BASE 
dw MapTile177 * 36 + TILES_BASE 
dw MapTile178 * 36 + TILES_BASE 
dw MapTile179 * 36 + TILES_BASE 
    ... 

   И вот такой рутиной обновляется: 
; Input: None 
; Output:None 
; Preserves: A', BC', DE', HL', IXH 
; Trashes: A, BC, DE, HL, IXL, IY 
UpdateMapAnimatedTiles: 
       ld a,(NumMapAnimatables)
       or a
       ret z
       pushAllowWrite MapAnimatables,
                     MAX_ANIMATABLES * 13
       pushAllowWrite MapTiles, 16*10*2
       pushAllowWrite MapDirty1,
                  MapDirtyEnd - MapDirty1
       ld hl,MapAnimatables
       ld ixl,a 
@@loop: ld c,(hl) ;delay 
       inc hl
       ld a,(hl) ;timer
       inc a     ;increase timer
       cp c      ;reached delay?
       jr z,@@1
       ld (hl),a ;store new timer value
       inc hl
       inc hl
       ld a,(hl) ;index
       jp @@3 
@@1:    xor a 
       ld (hl),a ;reset timer
       inc hl
       ld c,(hl) ;count
       inc hl
       ld a,(hl) ;index
       inc a     ;next frame
       inc a
       cp c      ;reached limit?
       jr nz,@@2
       xor a 
@@2:    ld (hl),a ;store new index 
@@3:    inc hl 
       ld e,(hl)
       inc hl
       ld d,(hl) ;DE = list of tiles
       inc hl
       ex de,hl ;save HL
       ld b,0    ;HL = list of tiles
       ld c,a
       add hl,bc ;add index to HL
       ld a,(hl) ;HL = new tile address
       inc hl
       ld h,(hl)
       ld l,a
       ex de,hl ;restore HL
       ld iy,32  ;DE = new tile address
       add iy,de ;IY = attribute address 
;load target address into BC 
       ld a,(hl) ;BC=offset into map data
       inc hl
       add a,0xff&MapData
       ld c,a
       ld a,(hl)
       adc a,0xff&(MapData>>8)
       ld b,a
       inc hl 
;write new tile to target address 
       ld a,e
       ld (bc),a
       inc bc
       ld a,d
       ld (bc),a 
;mark tile as dirty 
       ld b,0
       ld c,(hl) ;BC=offset into dirtymap
       inc hl 
;update dirty map 
       ex de,hl  ;save HL into DE
       ld hl,(MapDirtyBack)
       add hl,bc ;HL = corresponding line in MapDirty
       ld a,(de) ;first mask bit
       or (hl)   ;apply
       ld (hl),a ;store into dirty map
       inc hl
       inc de
       ld a,(de) ;second mask bit
       or (hl)   ;apply
       ld (hl),a ;store into dirty map
       inc de
       ex de,hl  ;restore HL 
;update attributes 
       ld e,(hl)
       inc hl
       ld d,(hl) ;DE = target screen addr
       inc hl
       ld b,iyh  ;BC = tile attributes
       ld c,iyl
       call DrawMapTileAttributes@@1 
;next iteration 
       dec ixl
       jr nz,@@loop
       popAllowWrite MapDirty1,
                  MapDirtyEnd - MapDirty1
       popAllowWrite MapTiles, 16*10*2
       popAllowWrite MapAnimatables,
                     MAX_ANIMATABLES * 13
       ret

   Суть процедуры состоит в том, чтобы:
  а) прописать  новый адрес тайла в распа─
кованные данные текущей карты в памяти;
  б) поставить грязный флаг для тайла,что─
бы при следующем обновлении экрана его пе─
рерисовала та же процедура,которая стирает
спрайты;
  в) заменить атрибуты на экране (процеду─
ра  стирания  тайлов  атрибуты не трогает,
чтобы сэкономить время,так как большинство
спрайтов красятся в цвет фона; коду, кото─
рому нужно перекрасить атрибуты,приходитсяcallDrawMapTileAttributesseparately).

Alone> 
 Do you have your own map editor? How's it going? 
Is it possible to link events to map points? 
Is there some kind of scripting system? 

Nikolai Zapolnov> 
 I have everything of my own: assembler, editor
sprites, map editor, full IDE. Includes
it also includes an emulator that has been added to a file
Fuse  (and integrates a debugger with it), com─ 
pilator  SDCC, bas2tap, tap2wav  and others
utilities. There is a graphics and tileset editor.
Tools for importing and exportingPNGand 
SCR.In fact, there are a lot of bugs, rakes, 
brakes and imperfections. I'm slowly when
I have a minute, I'm working on an improved version─
this.
   I needed a lot from the assembler─
cabbage soup, which was missing in the existing tools─
cops:
  1) generation of debugging informationfor
debugger (line in file, change addresses─
nykh - although I never finished them in debugging─
chick...).
  2) fast and convenient calculation of T-state for
instructions (they are displayed in my IDE
to the right of the line number).
  3) advanced macro tools (for example─
mer, I have meta-instructions for checking─
rks of going beyond memory limits, and my emulator─
Tor checks read/write operations; what-
something like valgrind for PC - lots of bugs─
small using them).

Alone> 
 These are macros  pushAllowWrite MapDirty1, 
MapDirtyEnd - MapDirty1? There is the address and length 
allowed recording addresses? 

Nikolai Zapolnov> 
 Yes, the first argument is the address, the second is
length.
   Macros  push push the resolution onto the stack
writes to the specified area pop remove them
from the stack (to pop you need to transfer the same
values, as in push - checking the emulator─
Yes, that orderpush/popis notbroken).
   Macros are registered in debug information─
tion and are linked to the address next to
them instructions. When the emulator does
instructions, he checks whether these are there
macros, and also executes them. And when it comes─
botka of memory read/write operations -
checks the current stack for the item, permission─
Is there a recording? If it's not allowed, stop it─
starts execution and throws it into the debugger, like this
same as breakpoint. You can see the code
registers, etc. and continue if desired
execution.
  4) expanded tools for working with
in sections. I can indicate the addresses to which
files write sections, compression section by section and
etc. Plus, the output generates a full
memory card, which helped me a lot.
 ... 
section bank2_langmenu 
[file "BANK2"] 
section z_intro_strings_ru 
[file "BANK2", compress=lzsa2] 
section z_intro_strings_en 
[file "BANK2", compress=lzsa2] 

section bank3 
[file "BANK3", base 0xc000] 

sectionbank4 
[file "BANK4", base 0xc000] 
section bank4_data_alien1 
[file "BANK4", compress=lzsa2] 
   ...

   Карта памяти выглядит так:

Data_tiles_22_A
 0xDE16/56854..0xDE33/56833 36/30 byte(s) 
...ещё 18 строк в таком же стиле... 

BANK1:imaginary> 
Bank1_music_buffer_bss
 0xE084/57476..0xFDE6/64998 7523 byte(s)
Bank1_engine_bss
 0xFDE7/64999..0xFFFF/65535 537 byte(s)

BANK2> 
   ...

Alone> 
  36/30 byte(s) - это  размер выделенного 
места и фактически занятое место? 

Николай Запольнов> 
   Да, первое число - оригинальный размер,
второе - после сжатия.
   На самом деле36-byte chunks are bad
example for compression :) These are single tiles,
which I screwed up anywhere already in the village─
last time to post more
tiles. They are scattered in different places in
memory, and some do not press well (you can
find, for example, 36/38, i.e. compressed version
more than the original); but add for
each tile flag whether it is compressed or not, and
making two branches of code turns out to be longer,
than losing a few bytes on uncompressed ones
taylah.

   For  bas2tap I added a couple of pseudoinsts─
manuals, for example, to embed assembly─
ler code directly into the instructionsREM.Like this
The source looks likeboot.bas:

0 REM @{loader} 
10 LET A=VAL"23635": RANDOMIZE USR(PEEK(A+ 
PI/PI)*VAL"256"+PEEK A+VAL"5") 

 The Lua engine is integrated into the collector, and
the entire assembly is written in Lua. Actually
there are the most terrible scripts for several thousand
lines in which the devil himself will break his leg :)
   Linking events to points on the case map─
is expressed through string values, which are par─
use a lua script. Each tile on the map
can be compareda line (or several),
by right-clicking. In the editor
maps they are indicated by yellow rectangles─
com with the inscription on top of the game screen.

Alone> 
And what happens as a result of the execution─ 
assembly script in Lua? 

Nikolai Zapolnov> 
 Scripts are not even generated by binaries, but
source code in assembler. Helps in debugging
scripts.
   Above is an example of sprite code - it
generated by a lua script; list of animated
tiles too. Here's an array, for example:
generated with information about enemies:

section bank0_data_enemies 
MapEnemyInfo: 
@@1: 
db 3 | (Offset_SpaceFlyingAlienAI << 2) 
 ;EnemyInfo_spriteCntAndAI 
db 64,104 ;EnemyInfo_origX, _origY 
db 0,0,24 ;EnemyInfo_x, _y, _h 
db 4 | (4<<3) | 0, 0, 0;_healthAndFlags, 
 ;_stateAndFlags, _time 
@@2: 
db 3 | (Offset_SpaceFlyingAlienAI << 2) 
 ;EnemyInfo_spriteCntAndAI 
db 176,104 ;EnemyInfo_origX, _origY 
db 0,0,24 ;EnemyInfo_x, _y, _h 
db 4 | (4<<3) | 0, 0, 0 ;_healthAndFlags, 
                    ;_stateAndFlags, _time
 ...

   In general, all sorts of data from sprites, maps
and other things in a form convenient for the engine.
   I also drew an auxiliary conclusion to myself:
for example, dumped information on which maps
there are certain tiles:

 ... 
gfx/tiles_NEW/12_06_lab_wall_center.gfx 
 09_05
 09_06
10_07 
gfx/tiles_NEW/00_06_starport_mid2.gfx 
 04_03
04_04
04_05 
gfx/tiles_NEW/01_00_human_bottle.gfx 
 10_05
 10_07
Zintr1 
gfx/tiles_NEW/01_00_human_bottle.gfx 
 10_05
 10_07
 Zintr1
 ...

   Or, for example, generated from
lua script the whole map inPNG,so that you can
was to evaluate how they fit together with─
fight, and look for mistakes.

Alone> 
 How are sprite animations described? B 
How are they edited? I wanted to apply 
Pixelorama,but it does not support firing─  
First mode and simultaneous editing 
several animations using common 
layers. And I didn’t understand how to unload from there 
delays with an accuracy of 1/50 s (GIFs are not like thissupports). 

Nikolai Zapolnov> 
 Animation editor, heh :)) I would like to.
So far, the frames and animation delays are described─
are in the code...

_AlienWalkSprites: 
 dw _alienGunWalkRight1
 dw _alienGunWalkRight2
 dw _alienGunWalkRight3
 dw _alienGunWalkRight4
 dw _alienGunWalkLeft1
 dw _alienGunWalkLeft2
 dw _alienGunWalkLeft3
 dw _alienGunWalkLeft4
 ... 
@@doWalk: 
 ld a,(ix+EnemyInfo_time)
rrca
rrca
 and 0x3f
 ld e,a
 ...

   For animated tiles - hard skinned
inmaps.lua:

If animName==gfx/tiles/acid/acid_anim.gfx 
oranimName==gfx/tiles/acid/ 
 acid_top_anim.gfx then
 delay = 5
deadlyArea[y * mapW + x] = true 
... 
elseif animName==gfx/level_elements/ 
 terminal_bottom_anim.gfx then
 delay = 10 
... 

 delay  falls into the card data block about
animated tiles (above was an example of sleep─
ska animated tiles).

Alone> 
 What does it mean DeadlyArea[y*MapW+x] = true?

Nikolay Zapolnov> 
DeadlyArea is an array of "deadly"
tiles. It is written there true for spikes and pussy─
lots andfalsefor regular tiles. They then
are saved by the script in a separate block, and
the engine checks the intersection of the character with
these tiles. If a player hits a tile 
DeadlyArea,he getswound. 

Alone> 
 Will the game sources be posted? By 
about their appearance - it's okay :) You, 
you've probably already seen what the sources look like 
which spektrumists usually post :) 

Nikolai Zapolnov> 
 I saw it. But here it also adds its own─
assembly language, Lua scripts generating
sprites, and other pleasures.
   Fonts are generated by one script and in
one format, tiles - another and in another
(plus there is hellish packaging and shuffling in
memory, was done in the end and in a hurry),
sprites - the third script, there is also a nes─
how many options - b/w and color.
   Sprites are unpacked and adjusted
so as to occupy the same addresses (on─
example, the code always addresses the player sprites
to the same addresses, and unpacked in their places─
there are sprites with a gun or in a spacesuit,
when the player selects the appropriate pre─
sweepings; same story with normal sprites─
nogo Elien and Elien with a pistol, for example─
measures).
   Besides, now everything is very much in my memory.
tight, I've also been there in recent days
chaos...

Share your thoughts about the article