Skip to content

Sesión 4: Estructuras de datos y objetos

Juan Gonzalez-Gomez edited this page Oct 19, 2021 · 36 revisions

Contenidos

Sesión 4.1: Registro IX (I)

Click to see the youtube video

Conceptualizar

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!!
  ret

El 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_loop

Generalizar

Tambié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      
  ret

Puesto 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

El registro IX

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)
...

Generalizando la función de dibujo de sprites

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 sprite

La 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
  ret

Y 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!

Pelota que rebota

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

Sesión 4.2: Registro IX (II)

Click to see the youtube video

Definiendo constantes

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, 6

Esta 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

Ejercicio: animaciones

  • 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)

Estado de animación variable con el tiempo

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, 10

Esta 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:

Estado de animación variable al moverse

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
  ret

Desde 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:

Sesión 4.3: Estructuras de datos con macros en ensamblador Z80

Click to see the youtube video

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

Clone this wiki locally