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