Repository navigation
Sesión 4: Estructuras de datos y objetos
- Enlace: kungfuguns
- Sesión 4.1: Registro IX (I)
- Sesión 4.2: Registro IX (II)
- Sesión 4.3: Estructuras de datos con macros en ensamblador Z80
Es importante mantener la integridad conceptual de nuestros programas. En cada lugar se tiene que hacer lo que se tiene que hacer, y no cosas diferentes. En nuestro main debe estar el bucle principal, y nada más. Por eso, la inicialización nos la llevamos a la función initialize_game
;;============================================
;; GAME INITIALIZATION
;;============================================
initialize_game:
;;-- Disable Amstrad firmware. Needed for changing
;;-- the video mode
call cpct_disableFirmware_asm
;;-- Change the video modeo to 0 (160x200 16 colors)
ld c,#0
call cpct_setVideoMode_asm
;;-- Change the color palette
ld hl, #_sprite_palette
ld de, #16
call cpct_setPalette_asm
;;-- Draw the map
;;-- Configure the tileset
ld hl,#_g_tileset
call cpct_etm_setTileset2x4_asm
;;-- Draw the level0
ld hl, #_level0
push hl
ld hl,#0xC000
push hl
ld bc,#0x0000 ;;-- Tile 0,0
ld de,#0x2028 ;;-- Height/Width in tiles (of the map)
ld a,#0x28 ;;-- Number of tiles in the row
call cpct_etm_drawTileBox2x4_asm ;; Draw the map!!
retEl programa principal queda así:
;;===============================
;; Main program entry
;;===============================
_main::
call initialize_game
main_loop:
call hero_erase ;; Erase the hero
call obstacle_erase ;; Erase the bullet
call hero_update ;; Update the Hero
call obstacle_update ;; Update the bullet
;;-- Check colision
call hero_getPtrHL
call obstacle_checkCollision
ld (0xC000),a ;;-- Draw collision
ld (0xC001),a
ld (0xC002),a
ld (0xC003),a
call hero_draw ;;-- Draw the hero
call obstacle_draw ;;-- Draw the bullet
;;-- Wait for the raster to finish
call cpct_waitVSYNC_asm
jr main_loopTambién es importante generalizar. Hay que tender a reutilizar el código. La desventaja de generalizar es que el código es menos eficiente. Se crea para cosas generales, por lo que no se pueden aprovechar los detalles para optimizar. Un código general no puede ser eficiente. La eficiencia se consigue siendo específico. Siempre tendremos ese compromiso entre Generalizar vs eficiencia
En nuestro código main, si quitamos el objeto en movimiento que colisiona con nuestro personaje, tenemos esto:
...
call hero_erase ;; Erase the hero
call hero_update ;; Update the Hero
call hero_draw ;;-- Draw the hero
...Este código sólo vale borrar, actualizar y dibujar nuestro personaje... pero ¿Y si queremos mover más objetos?. En la mayoria de ellos habrá que realizar también estas cosas: actualizarlos, borrarlos y dibujarlos
La parte de update sí que terminará siendo específica de cada entidad. En nuestro héroe estamos mirando el teclado,por ejemplo. Otras entidades no lo harán
Pero si miramos los datos del héroe, si que es una estructura generalizable
;;-- HERO DATA
hero_x: .db #39 ;;-- x position (in bytes [0-79])
hero_y: .db #80 ;;-- y posicion (in pixels [0-199])
hero_w: .db #4 ;;-- Width in bytes
hero_h: .db #12 ;;-- Height in bytes
hero_jump: .db #-1 ;;-- Are we jumping? El resto de entidades tendrán una posición y un tamaño. Lo tienen en común
Esto en C++ es lo que llamamos Herencia y composicion
Vamos a fijarnos en la función hero_draw. Sólo nos vale para dibujar al héroe:
;;-- Convert the hero_x, hero_y variables into
;;-- the corresponding address in the Video memory
ld de, #0xC000 ;;-- Video initial address
ld a,(hero_x)
ld c,a ;;-- Hero x position
ld a,(hero_y)
ld b,a ;;-- Hero y position
call cpct_getScreenPtr_asm
;; HL contains the video address
;; move it to the DE register
ex de, hl
;; Draw our hero
ld hl, #_sprite_smily
ld bc, #0x0C04 ;; Height, Width: 8x12 pixels
call cpct_drawSprite_asm
retPuesto que todas las entidades tendrán al menos esos mismos datos, lo que nos interesará es crear una función que permita pintar cualquier entidad,y no sólo al héroe
Los registros IX e IY se llaman registros índice. Con estos registros podemos hacer cosas como esta:
ld a,(ix)
ld a,(ix + 5)
Inicialmene el registro IX apunta a la estructura de la entidad. Al sumarle un offset constane estamos accediendo a los diferentes elementos de esa estructura. Siempre que a las entidades les demos la misma estructura, los offset serán los mismos, y podremos acceder a todos los elementos de cualquier entidad
Así,por ejemplo, con estas instrucciones podríamos leer todos los datos de cualquier entidad:
ld a,(ix + 0)
ld a,(ix + 1)
ld a,(ix + 2)
ld a,(ix + 3)La notacion usada en el sdcc es la misma que la del riscv:
ld a, 0(ix)
ld a, 1(ix)
ld a, 2(ix)
ld a, 3(ix)Exactamente lo mismos se puede hacer con el registro iY, lo que nos permite referenciar 2 entidades a la vez, por ejemplo para comprobar la colisión
También podemos hacer las mismas cosas que con el registro HL:
add 0(ix)
cp 1(ix)
inc 2(ix)
...Se propone como ejercicio el programar la función de dibujo del hero usando una función generalizada
Estos son los datos de la entidad del heroe y de otro (hero2) creada para comprobar el funcionamiento. El puntero del sprite de hero2 es la direccion 0000,por lo que va a dibujar cualquier cosa
;;-- HERO DATA
hero_data:
hero_x: .db #39 ;;-- (+0) x position (in bytes [0-79])
hero_y: .db #80 ;;-- (+1) y posicion (in pixels [0-199])
hero_w: .db #4 ;;-- (+2) Width in bytes
hero_h: .db #12 ;;--(+3) Height in bytes
hero_sprite: .dw #_sprite_smily ;;-- (+4) Pointer to the sprite
hero_jump: .db #-1 ;;-- Are we jumping?
hero2_data:
.db #60 ;;-- (+0) x position (in bytes [0-79])
.db #40 ;;-- (+1) y posicion (in pixels [0-199])
.db #4 ;;-- (+2) Width in bytes
.db #12 ;;--(+3) Height in bytes
.dw #0000 ;;-- (+4) Pointer to the spriteLa función hero_draw dibuja los dos sprites, llamando a entityDraw. Primero se carga en IX el puntero a la entidad, y luego se llama a la función genérica de dibujado
;;======================
;; Draw the hero
;; DESTROYS: AF, BC, HL
;;=======================
hero_draw::
ld ix, #hero_data
call entityDraw
ld ix, #hero2_data
call entityDraw
retY esta es la función entityDraw:
;;==============================
;; Draw an entity
;; INPUTS:
;; IX: Pointer to the entity
;;==============================
entityDraw:
;;-- Convert the entity's x and y coordinates into Video memory address
ld a,0(IX)
ld c,a ;;-- C = entity.x
ld a,1(IX)
ld b,a ;;-- B = entity.y
ld de, #0xC000 ;;-- Initial video address
call cpct_getScreenPtr_asm
;; HL contains the video address
;; move it to the DE register
ex de, hl
;;-- Draw the entity
ld l, 4(IX) ;;|
ld h, 5(IX) ;;| HL = entity.spritePtr
ld b, 3(IX) ;;-- B = entity.h
ld c, 2(IX) ;;-- B = entity.w
call cpct_drawSprite_asm
ret- Resultado ¡Funciona!

Voy a hacer el clásico ejemplo de la pelota que rebota... Lo voy a añadir al bucle principal. La pelota es un sprite de 4x4

Para acceder a los campos de las estructuras utilizamos constantes:
.equ Ent_x, 0
.equ Ent_y, 1
.equ Ent_w, 2
.equ Ent_h, 3
.equ Ent_spr_l, 4
.equ Ent_spr_h, 5
.equ Ent_jmp, 6Esta es la nueva función de dibujado, que además se ha unificado con la de borrado:
;;==============================
;; Draw an entity
;; INPUTS:
;; A : IF a==0, the entity is erased
;; IX: Pointer to the entity
;;==============================
entityDraw:
push AF
;;-- Convert the entity's x and y coordinates into Video memory address
ld a ,Ent_x(IX)
ld c ,a ;;-- C = entity.x
ld a ,Ent_y(IX)
ld b ,a ;;-- B = entity.y
ld de, #0xC000 ;;-- Initial video address
call cpct_getScreenPtr_asm
;; HL contains the video address
;; move it to the DE register
ex de, hl
ld b, Ent_h(IX) ;;-- B = entity.h
ld c, Ent_w(IX) ;;-- B = entity.w
pop af
cp #0 ; A==0?
jr z, erase_entity
;;-- Draw the entity
ld l, Ent_spr_l(IX) ;;|
ld h, Ent_spr_h(IX) ;;| HL = entity.spritePtr
call cpct_drawSprite_asm
ret
erase_entity:
call cpct_drawSolidBox_asm
ret- El personaje depende de su estado de animación:
- El estado de animación varia al moverlo (Ej. Animación de andar)
- El estado de aniación varía con el tiempo: cada fotograma dura un tiempo (Esta es más fácil)
En la estructura he añadido los dos sprites, un indice de sprite (0,1) y un contador
.equ CNT_INI, 20
.macro defineEntity name, x, y, w, h, spr, spr1
name'_data:
name'_x: .db x ;;-- position (in bytes [0-79])
name'_y: .db y ;;-- y posicion (in pixels [0-199])
name'_w: .db w ;;-- Width in bytes
name'_h: .db h ;;-- Height in bytes
name'_sprite: .dw spr ;;-- (+4) Pointer to the sprite
name'_sprite_1: .dw spr1 ;;-- Secon sprite
name'__sn: .db 0 ;;-- Sprite number to draw (0,1)
name'_cnt: .db CNT_INI ;;-- Animation counter
name'_jump: .db #-1 ;;-- Are we jumping?
.endm
.equ Ent_x, 0
.equ Ent_y, 1
.equ Ent_w, 2
.equ Ent_h, 3
.equ Ent_spr_l, 4
.equ Ent_spr_h, 5
.equ Ent_spr_l1, 6
.equ Ent_spr_h1, 7
.equ Ent_sn, 8
.equ Ent_cnt, 9
.equ Ent_jmp, 10Esta es la nueva rutina de redibujado. Se decrementa el contador y cuando llega a 0 se cambia el puntero de sprite
;;==============================
;; Draw an entity
;; INPUTS:
;; A : IF a==0, the entity is erased
;; IX: Pointer to the entity
;;==============================
entityDraw:
push AF
;;-- Convert the entity's x and y coordinates into Video memory address
ld a ,Ent_x(IX)
ld c ,a ;;-- C = entity.x
ld a ,Ent_y(IX)
ld b ,a ;;-- B = entity.y
ld de, #0xC000 ;;-- Initial video address
call cpct_getScreenPtr_asm
;; HL contains the video address
;; move it to the DE register
ex de, hl
ld b, Ent_h(IX) ;;-- B = entity.h
ld c, Ent_w(IX) ;;-- B = entity.w
pop af
cp #0 ; A==0?
jr z, erase_entity
;;-- Check the current counter
ld a, Ent_cnt(ix)
cp #0 ;-- Is it 0?
jr z,counter_0
;;-- Counter not zero. Just print
;;-- the current sprite
;;-- Decrement the counter
dec Ent_cnt(ix)
jr draw_current
;;-- Counter 0. Change the current sprite
counter_0:
ld a, Ent_sn(ix)
xor #1
ld Ent_sn(ix),a
ld a,#CNT_INI
ld Ent_cnt(ix),a
draw_current:
;;-- Draw the entity. Depending on the value of
;;-- the sn, sprite 0 or sprinte 1 is drawn
ld a, Ent_sn(ix) ;;-- Read the spite number to draw
cp #0 ;;-- Is it sprite 0?
jr z,draw_sprite_0
;-- It is sprite 1. Draw it!
ld l, Ent_spr_l1(IX) ;;|
ld h, Ent_spr_h1(IX) ;;| HL = entity.spritePtr
jr draw_sprite
;-- It is sprite 0. Draw it
draw_sprite_0:
ld l, Ent_spr_l(IX) ;;|
ld h, Ent_spr_h(IX) ;;| HL = entity.spritePtr
jr draw_sprite
draw_sprite:
ld Ent_sn(ix), a ;; Store the next sprite number
call cpct_drawSprite_asm
ret
erase_entity:
call cpct_drawSolidBox_asm
ret- Resultado:

Uso la misma estructura. Defino la función update_cnt para actualizar el contador y cambiar de sprite cuando llega a 0:
;;=========================================
;; Update the hero counter. When it reachs
;; 0 the sprite is changed
;;==========================================
update_cnt:
;;-- Check the current counter
ld a, Ent_cnt(ix)
cp #0 ;-- Is it 0?
jr z,counter_0
;;-- Counter not zero
;;-- Decrement the counter
dec Ent_cnt(ix)
ret
;;-- Counter 0. Change the current sprite
counter_0:
ld a, Ent_sn(ix)
xor #1
ld Ent_sn(ix),a
ld a,#CNT_INI
ld Ent_cnt(ix),a
retDesde moveHeroRight se llama a update_cnt cada vez que se incrementa la variable. Lo mismo desde moveHeroLeft, que es similar (de hecho se podrían unificar...)
;;=============================================
;; moveHeroRight: Move the hero to the right
;; (inside the line limits)
;;
;; DESTROYS:
;;=============================================
moveHeroRight:
;;- Check if the hero_x has reached its maximum value
;;-- We should check the possition 80 - hero.width
ld a, (hero_w)
ld b,a
ld a, #80
sub b ;;-- A = 80 - hero_w
ld b,a ;;-- B = 80 - hero_w
ld a, (hero_x) ;;-- A = hero_x
cp b ;; hero_x == 80-hero_w?
jr z,do_not_move_r
;;- No max reached: increase the hero_x by 1
inc a ;;-- A = A + 1
ld (hero_x),a ;;-- hero_x = A
call update_cnt
do_not_move_r:
ret- Resultado:

Las macros ya las he usado en el ejercicio anterior (tras ver este vídeo)


