es/SpriteCreationWithDyzen: Difference between revisions
(26 intermediate revisions by the same user not shown) | |||
Line 19: | Line 19: | ||
</li> | </li> | ||
<li> | <li> | ||
'''Sprite Main:''' Esta rutina es llamada en cada | '''Sprite Main:''' Esta rutina es llamada en cada Game Loop (SNES Frame) y se utiliza para actualizar al sprite y su logica. En pixi esta rutina inicia con la linea: | ||
<pre> | <pre> | ||
print "MAIN ",pc | print "MAIN ",pc | ||
Line 89: | Line 89: | ||
Utilizando una de las tablas Misc. de sprites que pueden ser usadas para cualquier proposito que desees. | Utilizando una de las tablas Misc. de sprites que pueden ser usadas para cualquier proposito que desees. | ||
Una vez hecho lo anterior necesitaremos poner esta variable con el valor 0, usaremos el estado 0 para nuestro estado inicial, ahora no siempre podemos iniciarlo en 0, podríamos por ejemplo, hacer que a través del extra byte o el extra property, darle el estado inicial al sprite, con el fin de generar distintas versiones del mismo sprite, pero lo más usual es usar el estado 0 como estado inicial. Para esto en | Una vez hecho lo anterior necesitaremos poner esta variable con el valor 0, usaremos el estado 0 para nuestro estado inicial, ahora no siempre podemos iniciarlo en 0, podríamos por ejemplo, hacer que a través del extra byte o el extra property, darle el estado inicial al sprite, con el fin de generar distintas versiones del mismo sprite, pero lo más usual es usar el estado 0 como estado inicial. Para esto en el '''SpriteInit''', agregaremos la linea: | ||
<pre> | <pre> | ||
STZ !State,x ;!State,x = 0 | STZ !State,x ;!State,x = 0 | ||
Line 112: | Line 112: | ||
</pre> | </pre> | ||
Podemos notar que esta rutina utiliza una tabla llamada "States", en esta tabla pondremos todos los estados que tenga nuestro sprite como correr, estar detenido, voltear, morir, etc. No es necesario ponerle de nombre a los estados "State0", simplemente usa el nombre que veas más conveniente, ahora normalmente se recomienda enumerarlos, ya que, sirve para recordar que valor de la variable <code>!State,x</code> llama a cada estado. | Podemos notar que esta rutina utiliza una tabla llamada "States", en esta tabla pondremos todos los estados que tenga nuestro sprite como correr, estar detenido, voltear, morir, etc. No es necesario ponerle de nombre a los estados "State0", simplemente usa el nombre que veas más conveniente, ahora normalmente se recomienda enumerarlos, ya que, sirve para recordar que valor de la variable <code>!State,x</code> llama a cada estado. | ||
Para aquellos que conocen algun lenguaje de programación de alto nivel, esta rutina seria el equivalente a: | Para aquellos que conocen algun lenguaje de programación de alto nivel, esta rutina seria el equivalente a: | ||
<pre> | <pre> | ||
Line 156: | Line 157: | ||
Otro detalle importante es notar que el estado al inicio tiene un <code>LDX !SpriteIndex</code>, esto se debe a que los sprites utilizan tablas para sus variables y estas tablas usan el índice del Sprite almacenado en el registro X para podes acceder al valor correcto de la tabla, sin embargo, cuando llamamos la rutina "StateMachine", el valor del registro X fue modificado y perdimos el índice del sprite en el registro X, así que usamos este comando para restaurarlo. | Otro detalle importante es notar que el estado al inicio tiene un <code>LDX !SpriteIndex</code>, esto se debe a que los sprites utilizan tablas para sus variables y estas tablas usan el índice del Sprite almacenado en el registro X para podes acceder al valor correcto de la tabla, sin embargo, cuando llamamos la rutina "StateMachine", el valor del registro X fue modificado y perdimos el índice del sprite en el registro X, así que usamos este comando para restaurarlo. | ||
Por ultimo, cabe la duda ¿Cómo se el índice | Por ultimo, cabe la duda ¿Cómo se el índice de la animación utilizada en el estado?, esta se responde yendo a la sección del código donde están las rutinas <code>ChangeAnimationFromStart</code>, ahí podremos encontrar algo como esto: | ||
<pre> | <pre> | ||
ChangeAnimationFromStart_Walk: | ChangeAnimationFromStart_Walk: | ||
Line 188: | Line 189: | ||
=== Condiciones y Transiciones === | === Condiciones y Transiciones === | ||
Las transiciones permiten que bajo ciertas condiciones el estado cambie a otro, cada estado podrá cambiar a un número limitado de otros estados con el fin que el sprite tenga el comportamiento deseado. Por ejemplo, si tuviéramos un sprite que simplemente se moviera de un lado a otro sin detectar precipicios, necesitaríamos una maquina de esta manera: | |||
[[File:Basic State Machine.png|thumb|center|Basic State Machine]] | |||
Tendríamos un sprite que caminaría en linea recta hasta toparse con una muralla, luego cambiaria al estado de volteo reproduciendo esa animación y cuando termine la animación de volteo entonces volvería al estado caminar yendo hacia el otro lado. | |||
Y en ASM esa maquina se veria de esta manera: | |||
<pre> | |||
StateMachine: | |||
LDA !State,x ;A Reg = !State,x | |||
ASL | |||
TAX ;Transform A Reg into a index value for table States and put that value in the X Reg | |||
JSR (States,x) ;Call Routine on the table States using X reg value. | |||
RTS | |||
States: | |||
dw Walk0 | |||
dw Flip1 | |||
Walk0: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
LDA !AnimationIndex,x | |||
CMP #$00 ;Index of the animation represented by the State | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Walk ;Change the animation to the animation with index #$XX | |||
LDA !GlobalFlip,x | |||
BEQ .right ;Check if is sprite direction is right or left | |||
LDA #$E0 ;If sprite direction is left then set negative X Speed | |||
BRA ++ | |||
.right | |||
LDA #$20 ;If sprite direction is right then set positive X speed | |||
++ | |||
STA !SpriteXSpeed,x | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
RTS | |||
.StateLoop | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
LDA !SpriteBlockedStatus_ASB0UDLR,x | |||
AND #$03 | |||
BEQ + ;Check if Left or Right wall are blocked | |||
LDA #$01 | |||
STA !State,x ;If left or right are blocked then change to state flip | |||
+ | |||
RTS | |||
Flip1: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
LDA !AnimationIndex,x | |||
CMP #$01 ;Index of the animation represented by the State | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Flip ;Change the animation to the animation with index #$XX | |||
STZ !SpriteXSpeed,x ;X Speed = 0 | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
RTS | |||
.StateLoop | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
LDA !AnimationFrameIndex,x | |||
CMP #$XX | |||
BCC + ;Checks the last frame of the animation | |||
LDA !AnimationTimer,x | |||
BEQ + ;Check if the frame finished | |||
LDA !GlobalFlip,x ;If animation finish | |||
EOR #$01 ; | |||
STA !GlobalFlip,x ;Alternate sprite direction | |||
STZ !State,x ;State = Walk | |||
JSR Walk0_StateStart ;Call Walk0 State Start | |||
+ | |||
RTS | |||
</pre> | |||
Analicemos el código. Primero podemos notar que tiene 2 estados como se muestra en el diagrama, el primero es Walk con índice 0 y el segundo es Flip con indice 1. Iniciemos viendo el estado Walk0, este tiene en el "StateStart" la siguiente condición: | |||
<pre> | |||
LDA !GlobalFlip,x | |||
BEQ .right ;Check if is sprite direction is right or left | |||
LDA #$E0 ;If sprite direction is left then set negative X Speed | |||
BRA ++ | |||
.right | |||
LDA #$20 ;If sprite direction is right then set positive X speed | |||
++ | |||
STA !SpriteXSpeed,x | |||
</pre> | |||
Esta condición primero revisa la variable <code>!GlobalFlip,x</code> que tiene el valor 0 si el sprite mira hacia la derecha o 1 si el sprite mira hacia la izquierda (puede ser al reves dependiendo de como se hizo el sprite en Dyzen), entonces si el sprite mira hacia la derecha (<code>!GlobalFlip,x es 0</code>) entonces carga en el registro A un valor positivo y en caso contrario carga en el registro A un valor negativo, luego de esto llama <code>STA !SpriteXSpeed,x</code> que setearia la velocidad del sprite y luego refresca la posición en pantalla del sprite con <code>JSL $01802A|!rom</code>. | |||
Luego en la sección "StateLoop" podemos notar el siguiente chequeo: | |||
<pre> | |||
LDA !SpriteBlockedStatus_ASB0UDLR,x | |||
AND #$43 | |||
BEQ + ;Check if Left or Right wall are blocked | |||
LDA #$01 | |||
STA !State,x ;If left or right are blocked then change to state flip | |||
+ | |||
</pre> | |||
Este chequeo revisaría los flags de la variable <code>!SpriteBlockedStatus_ASB0UDLR,x</code>, donde: | |||
*'''A:''' Flag que es 1 si el sprite esta tocando un bloque solido por encima en el Layer 2 (Detección de piso). | |||
*'''S:''' Flag que es 1 si el sprite esta tocando un bloque solido por los lados en el Layer 2 (Detección de paredes). | |||
*'''B:''' Flag que es 1 si el sprite esta tocando un bloque solido por abajo en el Layer 2 (Detección de techo). | |||
*'''U:''' Flag que es 1 si el sprite esta tocando un bloque solido por arriba en el Layer 1 (Detección de techo). | |||
*'''D:''' Flag que es 1 si el sprite esta tocando un bloque solido por abajo en el Layer 1 (Detección de piso). | |||
*'''R:''' Flag que es 1 si el sprite esta tocando un bloque solido por la derecha en el Layer 1 (Detección de pared derecha). | |||
*'''L:''' Flag que es 1 si el sprite esta tocando un bloque solido por la izquierda en el Layer 1 (Detección de pared izquierda). | |||
Usamos el comando <code>AND</code> para que todos los flags que no necesitamos queden en 0, en este caso estamos detectando paredes asi que los flags A, B, U y D no los necesitamos, por eso se usa el valor <code>#$43</code> que en binario seria <code>#$01000011</code>. De esta manera si el registro A luego de usar el and es distinto de 0 entonces esta tocando una pared. Luego de esto cambiaria el valor de <code>!State,x</code> para que use el estado Flip. | |||
Se debe destacar en este caso, que la rutina <code>JSL $01802A|!rom</code> actualiza el valor de <code>!SpriteBlockedStatus_ASB0UDLR,x</code>, por este motivo es llamada al inicio del "StateLoop" y no al final. | |||
Ahora revisaremos el estado Flip. Lo primero que debemos notar es que este también actualiza la posición del sprite, un incauto podría pensar que no necesita actualizar la posición durante la rutina Flip, ya que, el sprite no deberia moverse cuando esta tocando una muralla, sin embargo esto no es cierto y se debe a que se debe cubrir el caso en que el sprite no esta tocando el piso, por lo tanto, la posición vertical si debe ser actualizada. | |||
El "StateStart" en este caso es muy simple, solamente pone la velocidad horizontal en 0, lo interesante en este estado ocurre en el "StateLoop", aca podemos notar los siguientes chequeos: | |||
<pre> | |||
LDA !AnimationFrameIndex,x | |||
CMP #$XX | |||
BCC + ;Checks the last frame of the animation | |||
LDA !AnimationTimer,x | |||
BEQ + ;Check if the frame finished | |||
LDA !GlobalFlip,x ;If animation finish | |||
EOR #$01 ; | |||
STA !GlobalFlip,x ;Alternate sprite direction | |||
STZ !State,x ;State = Walk | |||
JSR Walk0_StateStart ;Call Walk0 State Start | |||
+ | |||
</pre> | |||
Iniciaremos con: | |||
<pre> | |||
LDA !AnimationFrameIndex,x | |||
CMP #$XX | |||
BCC + | |||
</pre> | |||
Este revisa la variable <code>!AnimationFrameIndex,x</code>, esta variable, podemos utilizarla para saber que frame dentro de la animación, se esta reproduciendo, por lo tanto, la idea seria revisar si el frame que se reproduce es el ultimo frame de la animación. ¿Cómo podemos saber que valor poner en #$XX? pues iremos a la tabla de esa animación, en la rutina de animación hay una tabla llamada "Frames", ejemplo: | |||
<pre> | |||
Frames: | |||
Animation0_Walk_Frames: | |||
db $00,$01,$02,$03,$04,$05,$06,$07 | |||
Animation1_Flip_Frames: | |||
db $08,$08 | |||
Animation2_Resist_Frames: | |||
db $09 | |||
Animation3_DeathLoop_Frames: | |||
db $0A,$0B | |||
Animation4_Death_Frames: | |||
db $0C,$0D,$0E,$0F,$10 | |||
</pre> | |||
En este ejemplo, podemos notar que la animación de Flip tiene solo 2 frames, entonces el valor que tendríamos que poner es #$01, (Tamaño de la animación - 1). | |||
Otro método para saber que número poner es ir a la tabla "AnimationLenght" que tiene el tamaño de cada animación, ejemplo: | |||
<pre> | |||
AnimationLenght: | |||
dw $0008,$0002,$0001,$0002,$0005 | |||
</pre> | |||
Podemos notar que en esta tabla se muestra el tamaño de cada una de las animaciones, en este caso: | |||
<pre> | |||
Walk => Tamaño 8 frames, ultimo frame #$07 | |||
Flip => Tamaño 2 frames, ultimo frame #$01 | |||
Resist => Tamaño 1 frames, ultimo frame #$00 | |||
DeathLoop => Tamaño 2 frames, ultimo frame #$01 | |||
Death => Tamaño 5 frames, ultimo frame #$04 | |||
</pre> | |||
Una vez entendido como funciona este chequeo, iremos al siguiente: | |||
<pre> | |||
LDA !AnimationTimer,x | |||
BEQ + | |||
</pre> | |||
Este es bastante simple, lo que indica la variable <code>!AnimationTimer,x</code> es la cantidad de Game Loops (SNES Frames) para que el frame cambie, por lo tanto, si tiene un valor distinto de 0, significa que el frame aun se esta reproduciendo. | |||
Luego de estos 2 checks usamos los comandos: | |||
<pre> | |||
LDA !GlobalFlip,x ;If animation finish | |||
EOR #$01 ; | |||
STA !GlobalFlip,x ;Alternate sprite direction | |||
</pre> | |||
El comando <code>EOR</code> puede ser utilizado para alternar bits del registro A, por ejemplo, <code>EOR #$01</code> alternaría el bit de más a la derecha, entonces si ese bit era un 0 lo cambiaria a un 1 y si era un 1, lo cambiaria a 0. usamos esto para alternar la dirección del sprite. | |||
Por ultimo se pone se vuelve al estado Walk pero además se llama al "StateStart" del estado walk, la razón de esto es que al alternar la dirección del sprite, ocurriría que por 1 frame el sprite muestre la animación volteada incorrectamente, por esto, en los estados de volteo es muy común llamar el "StateStart". | |||
Aca podemos notar el resultado de este comportamiento: | |||
[[File:Kritter.gif|thumb|center|Kritter]] | |||
== Movimiento == | == Movimiento == | ||
Line 193: | Line 383: | ||
=== Movimiento Avanzado === | === Movimiento Avanzado === | ||
== Interacción == | == Interacción == | ||
=== | Dyzen tiene su propio sistema de interacción, sin embargo, este no es obligatorio de usar, por lo que, se profundizara en las distintas maneras de realizar la interacción entre el Sprite y otras entidades del juego como puede ser el player, proyectiles, otros sprites, etc. | ||
En esta sección además veremos ciertas acciones que se pueden realizar cuando la colisión es detectada. | |||
=== Detección de colisión con Player === | |||
==== Vanilla ==== | ==== Vanilla ==== | ||
Para detectar si el sprite interactua con el player, primero debemos en el CFG Editor seleccionar el Sprite Clipping que deseamos, este aparecera en el cuadrado azul debajo: | |||
[[File:Sprite Clipping.png|thumb|center|Sprite Clipping]] | |||
Debemos considerar que el cuadrado que celeste oscuro que sale seria equivalente a el cuadrado de borde rojo que sale en el centro en Dyzen: | |||
[[File:Dyzen Red Square.png|thumb|center|Dyzen Red Square]] | |||
Por lo tanto, cuando hagas el sprite en Dyzen debes considerar esto para poner los gráficos en la posición correcta para que calce con la Hitbox. | |||
Si tu sprite tiene una interacción común y corriente, puede que no necesites hacer nada en el ASM, mientras mantengas la casilla "Don't use default interactión with Mario" en blanco. Pero si necesitas realizar algo fuera de lo común puedes usar los siguientes comandos: | |||
<pre> | |||
JSL $03B664|!rom ;Load Player Hitbox/Clipping | |||
JSL $03B69F|!rom ;Load Sprite Hitbox/Clipping | |||
JSL $03B72B|!rom ;Check For contact between Player and Sprite | |||
BCC + | |||
.ThereIsContact | |||
;Here put the code that happends when contact between Player and Sprite exists. | |||
+ | |||
</pre> | |||
Como podemos notar, primero se carga la caja de colisión del player, luego se carga la del sprite y luego se llama otra rutina para verificar si existe contacto entre ambos. | |||
==== Custom ==== | ==== Custom ==== | ||
Para una rutina personalizada es practicamente lo mismo que en la rutina vanilla, lo que cambia es que debes cargar la caja de colisión del sprite manualmente, para esto podemos utilizar las siguientes direcciones de RAM: | |||
*$00 (!Scratch0): Low byte de la posición X de la caja de colisión. | |||
*$01 (!Scratch1): Low byte de la posición Y de la caja de colisión. | |||
*$02 (!Scratch2): Ancho de la caja de colisión. | |||
*$03 (!Scratch3): Alto de la caja de colisión. | |||
*$08 (!Scratch8): High Byte de la posición X de la caja de colisión. | |||
*$09 (!Scratch9): High Byte de la posición Y de la caja de colisión. | |||
Por lo tanto la manera adecuada de usarlo seria algo como lo siguiente: | |||
<pre> | |||
JSL $03B664|!rom ;Load Player Hitbox/Clipping | |||
LDA !SpriteXHigh,x ;Load Sprite X High Byte | |||
XBA ; | |||
LDA !SpriteXLow,x ;Load Sprite X Low Byte | |||
REP #$20 ;A 16 bits = X position | |||
CLC | |||
ADC #!OffsetX ;A = Sprite X position + Hitbox X Offset (Offset must be 16 bits) | |||
SEP #$20 | |||
STA !Scratch0 ;Store Hitbox X Low Byte | |||
XBA | |||
STA !Scratch8 ;Store Hitbox X Low Byte | |||
LDA !SpriteYHigh,x ;Load Sprite Y High Byte | |||
XBA ; | |||
LDA !SpriteYLow,x ;Load Sprite Y Low Byte | |||
REP #$20 ;A 16 bits = Y position | |||
CLC | |||
ADC #!OffsetY ;A = Sprite Y position + Hitbox Y Offset (Offset must be 16 bits) | |||
SEP #$20 | |||
STA !Scratch1 ;Store Hitbox Y Low Byte | |||
XBA | |||
STA !Scratch9 ;Store Hitbox Y Low Byte | |||
LDA #!Width | |||
STA !Scratch2 ;Store Hitbox Width | |||
LDA #!Height | |||
STA !Scratch3 ;Store Hitbox Height | |||
JSL $03B72B|!rom ;Check For contact between Player and Sprite | |||
BCC + | |||
.ThereIsContact | |||
;Here put the code that happends when contact between Player and Sprite exists. | |||
+ | |||
</pre> | |||
Como podemos notar, es la misma rutina, solo que esta vez los parametros los entregamos de forma manual en ves de definirlos en el CFG. Basicamente se carga la posición de la hitbox (Hitbox Offset + Sprite Position) y se carga el ancho y alto de la caja. | |||
==== Interacción con Dyzen ==== | ==== Interacción con Dyzen ==== | ||
=== | El sistema de Dyzen es una generalización de la interacción personalizada, solo que en ves de entregar los parametros de esa manera, hace un loop que verifica distintas cajas de colisión de una tabla. En el caso de este sistema cada caja de colisión tiene una acción que es definida en el tool en la sección "Interaction" de Dyzen: | ||
[[File:Dyzen Hitbox Action.png|thumb|center|Dyzen Hitbox Action]] | |||
Entonces esa acción es llamada cuando existe contacto entre la caja de colisión respectiva y el player. | |||
=== Detección de colisión con Otros Sprites === | |||
==== Vanilla ==== | ==== Vanilla ==== | ||
Para detectar colisión sprite<->sprite, es muy similar a la colisión con player, lo que cambia es que ahora se deben revisar todos los slots de sprites para detectar esa colisión. | |||
El código seria como el siguiente: | |||
<pre> | |||
InteractionWithSprites: | |||
JSL $03B69F|!rom ;Load Sprite Hitbox/Clipping | |||
LDX #!MaxSprites-1 ;Start the loop from the end of the sprites table | |||
.loop | |||
CPX !SpriteIndex | |||
BEQ .next ;Skip loop if the Sprite index is the same than the current sprite | |||
JSL $03B6E5|!rom ;Load the other Sprite Hitbox/Clipping | |||
JSL $03B72B|!rom ;Check For contact between Player and Sprite | |||
BCC .next | |||
.ThereIsContact | |||
PHX ;Preserve X Register value | |||
TXY ;The index of the other sprite is saved in Y Register | |||
LDX !SpriteIndex ;Load Sprite index of the current sprite | |||
;Here put the code that happends when contact between Player and Sprite exists. | |||
PLX ;Restore X register Value | |||
.next | |||
DEX | |||
BPL .loop | |||
LDX !SpriteIndex ;Restore Sprite Index | |||
</pre> | |||
Como se puede ver, seria un Loop que que revisa cada uno de los sprites y revisa si existe colisión entre ambos. Se recomienda antes del <code>JSL $03B6E5|!rom</code> hacer un check del <code>!SpriteNumber</code> o del <code>!CustomSpriteNumber</code> para que solo colisione con los sprites que deseas y no con absolutamente todos. Tambien recuerda poner un check de la variable <code>!SpriteStatus</code>, ya que, puede que solo te interese detectar la colisión con sprites que no estan muertos. | |||
==== Custom ==== | ==== Custom ==== | ||
Para hacer una hitbox personalizada, haremos lo mismo que en el caso anterior solo que esta vez en ves de llamar a <code>JSL $03B69F|!rom</code>, codificaremos nosotros mismo la hitbox como se vio en '''Detección de colisión con Player/Custom'''. | |||
<pre> | |||
InteractionWithSprites: | |||
LDA !SpriteXHigh,x ;Load Sprite X High Byte | |||
XBA ; | |||
LDA !SpriteXLow,x ;Load Sprite X Low Byte | |||
REP #$20 ;A 16 bits = X position | |||
CLC | |||
ADC #!OffsetX ;A = Sprite X position + Hitbox X Offset (Offset must be 16 bits) | |||
SEP #$20 | |||
STA !Scratch0 ;Store Hitbox X Low Byte | |||
XBA | |||
STA !Scratch8 ;Store Hitbox X Low Byte | |||
LDA !SpriteYHigh,x ;Load Sprite Y High Byte | |||
XBA ; | |||
LDA !SpriteYLow,x ;Load Sprite Y Low Byte | |||
REP #$20 ;A 16 bits = Y position | |||
CLC | |||
ADC #!OffsetY ;A = Sprite Y position + Hitbox Y Offset (Offset must be 16 bits) | |||
SEP #$20 | |||
STA !Scratch1 ;Store Hitbox Y Low Byte | |||
XBA | |||
STA !Scratch9 ;Store Hitbox Y Low Byte | |||
LDA #!Width | |||
STA !Scratch2 ;Store Hitbox Width | |||
LDA #!Height | |||
STA !Scratch3 ;Store Hitbox Height | |||
LDX #!MaxSprites-1 ;Start the loop from the end of the sprites table | |||
.loop | |||
CPX !SpriteIndex | |||
BEQ .next ;Skip loop if the Sprite index is the same than the current sprite | |||
JSL $03B6E5|!rom ;Load the other Sprite Hitbox/Clipping | |||
JSL $03B72B|!rom ;Check For contact between Player and Sprite | |||
BCC .next | |||
.ThereIsContact | |||
PHX ;Preserve X Register value | |||
TXY ;The index of the other sprite is saved in Y Register | |||
LDX !SpriteIndex ;Load Sprite index of the current sprite | |||
;Here put the code that happends when contact between Player and Sprite exists. | |||
PLX ;Restore X register Value | |||
.next | |||
DEX | |||
BPL .loop | |||
LDX !SpriteIndex ;Restore Sprite Index | |||
</pre> | |||
El problema de este método es que si otro tiene una rutina de colisión sprite<->sprite e intenta detectar a nuestro enemigo, esa colisión seguirá usando el Sprite Clipping Vanilla, por lo tanto, para evitar esto, le pondremos en el CFG '''"Don't Interact with other sprites"'''. | |||
==== Interacción con Dyzen ==== | ==== Interacción con Dyzen ==== | ||
== Rutina Gráfica == | == Rutina Gráfica == | ||
=== Trucos en la Rutina Gráfica === | === Trucos en la Rutina Gráfica === | ||
Line 208: | Line 552: | ||
== Comportamientos Comunes == | == Comportamientos Comunes == | ||
=== Comportamientos durante el Funcionamiento Normal === | === Comportamientos durante el Funcionamiento Normal === | ||
==== Voltear cuando detecta un Precipicio ==== | |||
==== Perseguir al Player ==== | |||
==== Saltar ==== | |||
Normalmente para realizar un salto tenemos una animación de salto, otra animación que ocurre mientras el sprite esta cayendo y una ultima que ocurre cuando el sprite aterriza en el piso. Por esto y siguiendo las ideas del capitulo de Maquinas de Estado, crearemos 3 estados: | |||
*'''Jump:''' Es el estado que realiza el salto. | |||
*'''Fall:''' Es el estado que comienza cuando el sprite esta cayendo. | |||
*'''Arrive:''' Es el estado que comienza cuando el sprite llega al piso. | |||
Para este ejemplo, asumiremos que las animaciones de salto son las con índice <code>#$01</code>, <code>#$02</code> y <code>#$03</code> respectivamente. La animación de Jump y Arrive deberían ser '''Only Once''', mientras que Fall deberia ser '''Continuous'''. | |||
Primero se crea la maquina de estados con los 3 estados, ahora, además de estos estados debemos crear un Idle, asi que serian 4 estados, el idle lo utilizaremos para gatillar el salto. | |||
<pre> | |||
StateMachine: | |||
LDA !State,x ;A Reg = !State,x | |||
ASL | |||
TAX ;Transform A Reg into a index value for table States and put that value in the X Reg | |||
JSR (States,x) ;Call Routine on the table States using X reg value. | |||
RTS | |||
States: | |||
dw Idle0 | |||
dw Jump1 | |||
dw Fall2 | |||
dw Arrive3 | |||
</pre> | |||
El estado Idle sera muy simple, solamente detectara si hay colisión con el piso y si es así, pasara al estado jump. Si leíste el capitulo de Maquinas de estados esto no debería ser un problema. El estado Idle seria el siguiente: | |||
<pre> | |||
Idle0: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
LDA !AnimationIndex,x ;Index of the animation represented by the State | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Idle ;Change the animation to the animation with index #$XX | |||
RTS | |||
.StateLoop | |||
LDA !SpriteBlockedStatus_ASB0UDLR,x | |||
AND #$24 | |||
BEQ + ;Check if the sprite is touching the floor. | |||
LDA #$01 | |||
STA !State,x ;Change to state Jump | |||
+ | |||
RTS | |||
</pre> | |||
Ahora crearemos el estado de Jump. Este estado debe ponerle un valor negativo a la velocidad en Y al inicio y cambiar la animación a Jump, luego, cuando detecta que la velocidad en Y es 0 o positiva, cambiar al estado Fall, recuerda que en SMW Velocidad en Y negativa es ir hacia arriba. | |||
<pre> | |||
Jump1: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
LDA !AnimationIndex,x ;Index of the animation represented by the State | |||
CMP #$01 | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Jump ;Change the animation to the animation with index #$XX | |||
LDA #$D0 | |||
STA !SpriteYSpeed,x ;Set Y Speed to a negative value | |||
RTS | |||
.StateLoop | |||
LDA !SpriteYSpeed,x | |||
BMI + ;Check if Y Speed is Positive or negative. | |||
LDA #$02 ;If Y Speed >= 0 then | |||
STA !State,x ;Change to State Fall | |||
+ | |||
RTS | |||
</pre> | |||
Luego el estado Fall, solo moveria al sprite y cuando detecta el piso cambia al estado Arrive, es muy similar al estado Idle0. | |||
<pre> | |||
Fall2: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
LDA !AnimationIndex,x ;Index of the animation represented by the State | |||
CMP #$02 | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Fall ;Change the animation to the animation with index #$XX | |||
RTS | |||
.StateLoop | |||
LDA !SpriteBlockedStatus_ASB0UDLR,x | |||
AND #$24 | |||
BEQ + ;Check if the sprite is touching the floor. | |||
LDA #$03 | |||
STA !State,x ;Change to state Arrive | |||
+ | |||
RTS | |||
</pre> | |||
Por ultimo, el estado arrive debería detectar que la animación termino y cambiar a idle, esto tambien esta documentado en la sección de Maquinas de estado, se vería de la siguiente manera. Asumiremos que la animación de arrive tiene 3 frames asi que el ultimo frame sera el <code>#$02</code> | |||
<pre> | |||
Arrive3: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
LDA !AnimationIndex,x | |||
CMP #$03 ;Index of the animation represented by the State | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Arrive ;Change the animation to the animation with index #$XX | |||
RTS | |||
.StateLoop | |||
LDA !AnimationFrameIndex,x | |||
CMP #$02 | |||
BCC + ;Checks the last frame of the animation | |||
LDA !AnimationTimer,x | |||
BEQ + ;Check if the frame finished | |||
STZ !State,x ;State = Idle | |||
+ | |||
RTS | |||
</pre> | |||
Esta maquina de estados, haria al sprite saltar apenas toque el piso teniendo 4 animaciones distintas (Idle, Jump, Fall y Arrive), puedes en el estado Idle incluir timers o condiciones para que salte solo cuando tu lo deseas. | |||
Probablemente estés pensando, que sucede si salta y además se mueve en horizontal. Lo que haremos en este caso es en el estado Idle, poner la Velocidad X en 0, luego en el estado Jump además en el '''StateInit''' además de ponerle la velocidad en Y, pondremos la velocidad en X, luego de esto tenemos varias opciones que deben realizarse '''StateStart''' de Jump como de Fall cuando detecte una pared: | |||
*Poner la velocidad X en 0 y cuando el sprite llegue al piso, voltearlo. | |||
*Voltear el Sprite y poner la velocidad X en el mismo valor con el signo opuesto. | |||
*Se pase a un estado que gestione esa interacción. | |||
Todas las opciones son validas y dependerá de tu sprite cual elegir. | |||
==== Daño y Muerte ==== | |||
Para hacer este comportamiento pensaremos en un sprite como de Donkey Kong Country, que cuando muere se eleva un poco y luego cae fuera de pantalla. Para esto necesitaremos 3 estados: | |||
*'''Hurt:''' Es el estado que ocurre cuando el sprite recibe daño. | |||
*'''Dead:''' Es el estado que ocurre cuando el sprite muere elevándose por el aire. | |||
*'''DeadFinish:''' Es el estado que ocurre cuando el sprite llega a su punto más alto y empieza a caer. | |||
Para este ejemplo, asumiremos que las animaciones de Hurt, Dead y DeadFinish son <code>#$01</code>,<code>#$02</code> y <code>#$03</code> respectivamente. | |||
Antes de empezar, necesitaremos una variable que almacene la cantidad de vida (HP o hitpoints) que le quedan al enemigo. Para esto, en la sección de defines podemos crear el siguiente define: | |||
<pre> | |||
!Hitpoints = !SpriteMiscTable9 | |||
</pre> | |||
Luego en el '''Sprite Init''' podemos poner la cantidad de vida que tiene. Para esto podemos utilizar tanto una constante o usar un extra byte para el hp inicial, en mi caso usare una constante. | |||
<pre> | |||
LDA #$03 | |||
STA !Hitpoints,x | |||
</pre> | |||
Luego de esto, crearemos una maquina de estado como esta: | |||
<pre> | |||
StateMachine: | |||
LDA !State,x ;A Reg = !State,x | |||
ASL | |||
TAX ;Transform A Reg into a index value for table States and put that value in the X Reg | |||
JSR (States,x) ;Call Routine on the table States using X reg value. | |||
RTS | |||
States: | |||
dw Idle0 | |||
dw Hurt1 | |||
dw Dead2 | |||
dw DeadFinish3 | |||
</pre> | |||
El estado de Idle es irrelevante para este comportamiento, ya que, normalmente gatillaremos el estado Hurt1 desde algún tipo de interacción con el player o con ciertos objetos del juego, asi que empezaremos con el estado Hurt: | |||
<pre> | |||
Hurt1: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
LDA !Hitpoints,x | |||
CMP #$01 | |||
BCS + | |||
STZ !Hitpoints,x ;if Hitpoints = 1 then go to state dead and hp = 0. | |||
LDA #$02 | |||
STA !State,x | |||
JMP Dead2 | |||
+ | |||
JSL $01802A|!rom ;Update Sprite position with gravity | |||
LDA !AnimationIndex,x ;Index of the animation represented by the State | |||
CMP #$01 | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Hurt ;Change the animation to the animation with index #$XX | |||
DEC !Hitpoints,x | |||
RTS | |||
.StateLoop | |||
LDA !AnimationFrameIndex,x | |||
CMP #$02 | |||
BCC + ;Checks the last frame of the animation | |||
LDA !AnimationTimer,x | |||
BEQ + ;Check if the frame finished | |||
STZ !State,x ;Return to state 0 (Idle) | |||
RTS | |||
</pre> | |||
En este caso asumimos que la animación de daño tiene 3 frames, lo que hace este estado es: | |||
# Si le queda 1 de hp al recibir daño, el sprite pasara al estado de muerte y los hitpoints pasaran a ser 0. | |||
# Si al sprite aun le queda hp, lo disminuye en 1 y utiliza la animación de daño. | |||
# Una vez que termine la animación de daño, pasa al estado Idle. | |||
Para llegar a este estado, usando el método que desees para dañar al sprite (ya sea saltándole encima, lanzándole caparazones o cualquier otro), en esa interacción debes cambiar el estado al estado Hurt. En este ejemplo seria con: | |||
<pre> | |||
LDA #$01 | |||
STA !State,x | |||
</pre> | |||
Luego el estado de Dead seria como el siguiente: | |||
<pre> | |||
Dead2: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
LDA !AnimationIndex,x ;Index of the animation represented by the State | |||
CMP #$02 | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_Dead ;Change the animation to the animation with index #$XX | |||
LDA #$D0 | |||
STA !SpriteYSpeed,x ;Set Y Speed as Negative | |||
LDA #$02 | |||
STA !SpriteStatus,x ;Set Sprite Status to 2 (dying) | |||
RTS | |||
.StateLoop | |||
LDA !SpriteYSpeed,x | |||
BMI + ;Check if Y Speed is Positive or negative. | |||
LDA #$03 ;If Y Speed >= 0 then | |||
STA !State,x ;Change to State DeadFinish | |||
+ | |||
RTS | |||
</pre> | |||
Basicamente pone la velocidad en Y en un valor negativo, pone el <code>!SpriteStatus,x</code> en 2 (muerte), reproduciría la animación de muerte mientras sube y cuando la velocidad en Y se vuelve 0 o positiva, pasa al estado DeadFinish. | |||
Debemos notar que este estado no utiliza la rutina de actualización de movimiento, esto se debe a que cuando el <code>!SpriteStatus,x</code> es 2, el juego actualiza su posición de manera automatica. | |||
Por ultimo, tendriamos el estado "DeadFinish" que simplemente es un estado que solo cambia de animación. | |||
<pre> | |||
DeadFinish3: | |||
LDX !SpriteIndex ;Load Sprite index on X Register | |||
LDA !AnimationIndex,x ;Index of the animation represented by the State | |||
CMP #$03 | |||
BEQ .StateLoop | |||
.StateStart | |||
JSR ChangeAnimationFromStart_DeadFinish ;Change the animation to the animation with index #$XX | |||
RTS | |||
.StateLoop | |||
RTS | |||
</pre> | |||
Esto permitiría una muerte como las que se usan en el Donkey Kong Country donde el enemigo tiene una animación cuando recibe daño (en caso de tener HP), otra animación cuando empieza a morir saltando hacia arriba y luego cuando cae fuera de la pantalla usa otra animación. | |||
Haciendo variaciones en este comportamiento probablemente puedas crear muertes más simples o más complejas, pero ya dependería de la creatividad de cada uno. | |||
=== Comportamientos durante la Interacción con el Player === | === Comportamientos durante la Interacción con el Player === | ||
=== | En este capitulo se veran comportamientos comunes que ocurren cuando se detecta colisión entre el player y una hitbox del sprite, se recomienda revisar primero el capitulo de '''Interacción'''. | ||
Algo que debe ser considerado en esta sección es que, las cajas de colisión estaran guardada en la '''Scratch Rams''' que van desde '''$00 a $0B'''. | |||
Los datos de la caja del player estaran en: | |||
*'''$00:''' Low Byte de la Posición X de la caja de colisión. | |||
*'''$01:''' Low Byte de la Posición Y de la caja de colisión. | |||
*'''$02:''' Ancho de la caja de colisión. | |||
*'''$03:''' Alto de la caja de colisión. | |||
*'''$08:''' High Byte de la Posición X de la caja de colisión. | |||
*'''$09:''' High Byte de la Posición Y de la caja de colisión. | |||
Mientras que los datos de la caja de colisión del sprite estarán en: | |||
*'''$04:''' Low Byte de la Posición X de la caja de colisión. | |||
*'''$05:''' Low Byte de la Posición Y de la caja de colisión. | |||
*'''$06:''' Ancho de la caja de colisión. | |||
*'''$07:''' Alto de la caja de colisión. | |||
*'''$0A:''' High Byte de la Posición X de la caja de colisión. | |||
*'''$0B:''' High Byte de la Posición Y de la caja de colisión. | |||
==== Detección del Player ==== | |||
Esta es una idea que puede ayudarte para que el sprite cambie su comportamiento cuando el player esta en cierta posición con respecto al player. La idea es tener una caja de colisión y cuando esa caja es detectada, pasar a cierto estado. Por ejemplo, supongamos que el estado disparar (asumiremos que es el estado <code>#$01</code> para este ejemplo, aunque puede ser cualquier estado realmente) es gatillado cuando el player esta en cierta área, podemos llamar a una rutina como la siguiente: | |||
<pre> | |||
ShootTrigger: | |||
LDX !SpriteIndex ;Restore X register | |||
LDA #$01 | |||
STA !State,x ;change state to Shoot State. | |||
RTS | |||
</pre> | |||
Esta función simple, haria que el sprite pase al estado de disparo cuando el sprite esta en cierta posición. Se recomienda para esto usar una rutina de interacción custom o la que viene en Dyzen. | |||
Se debe notar que la linea <code>LDX !SpriteIndex</code>, no es del todo necesaria dependiendo de tu sistema de interacción, pero el sistema de Dyzen, cuando usa múltiples cajas de colisión con distintas acciones, requiere esta linea, ya que, se pierde el el valor del registro X. | |||
==== Dañar al Player ==== | |||
Para dañar al player, podrias pensar en solo usar la linea <code>JSL $00F5B7|!rom</code>, sin embargo, esta tiene un problema, que cuando el player esta montado sobre yoshi, en ciertas ocasiones, en ves de hacer que yoshi se escape, hace que el player se achique, por lo tanto, te recomiendo las siguientes rutinas que puedes poner en la carpeta <code>routines</code> de pixi en un archivo ".asm" con el nombre DamagePlayer. | |||
<pre> | |||
PHX | |||
LDA $187A|!addr ;if the player is not riding yoshi then damage the player | |||
BEQ ?+ ;otherwise dismount yoshi | |||
JSR ?FindYoshi | |||
BCC ?+ | |||
JSR ?DismountYoshi | |||
PLX | |||
RTL | |||
?+ | |||
JSL $00F5B7|!rom | |||
PLX | |||
RTL | |||
?FindYoshi: | |||
LDX $18DF|!addr | |||
BEQ ?.crawlForYoshi | |||
DEX | |||
BRA ?.found | |||
?.crawlForYoshi: | |||
LDX.w $1692|!addr | |||
; Start Slot according to sprite data | |||
LDA.l $02A773|!rom,x | |||
SEC | |||
SBC #$FE ; spaces 2 reserved slots, have to interact with them too | |||
TAX | |||
?.loop: | |||
LDA !SpriteNumber,x | |||
CMP #$35 | |||
BNE ?.continueLoop | |||
LDA !SpriteStatus,x | |||
BNE ?.found | |||
?.continueLoop | |||
DEX | |||
BPL ?.loop | |||
?.returnClear: | |||
CLC | |||
RTS | |||
?.found | |||
SEC | |||
RTS | |||
?DismountYoshi: | |||
LDA #$10 | |||
STA !SpriteDecTimer6,x | |||
LDA #$03 ; \ Play sound effect | |||
STA $1DFA|!addr ; / | |||
LDA #$13 ; \ Play sound effect | |||
STA $1DFC|!addr ; / | |||
LDA #$02 | |||
STA !SpriteMiscTable3,x | |||
STZ $187A|!addr | |||
LDA #$C0 | |||
STA !PlayerYSpeed | |||
STZ !PlayerXSpeed | |||
%SubHorzPos() | |||
LDA ?XSpeedDismountTable,y | |||
STA !SpriteXSpeed,X | |||
STZ !SpriteMiscTable10,x | |||
STZ !SpriteMiscTable6,X | |||
STZ $18AE|!addr | |||
STZ $0DC1|!addr | |||
LDA #$30 ; \ Mario invincible timer = #$30 | |||
STA $1497|!addr ; / | |||
JSR ?CODE_01EDCC | |||
RTS ; Return | |||
?XSpeedDismountTable: | |||
db $E8,$18 | |||
?CODE_01EDCC: | |||
LDY.B #$00 | |||
LDA !SpriteYLow,X | |||
SEC | |||
SBC ?YoshiOffset,Y | |||
STA !PlayerY | |||
STA $D3 | |||
LDA !SpriteYHigh,X | |||
SBC #$00 | |||
STA !PlayerY+$01 | |||
STA $D4 | |||
RTS ; Return | |||
?YoshiOffset: | |||
db $04,$10 | |||
</pre> | |||
Esta rutina hara que el si yoshi existe en el nivel y el player lo esta montando, entonces, en ves de dañar al player, haga que yoshi se escape. Luego puedes usar una rutina como esta para dañar al player de forma correcta. | |||
<pre> | |||
HurtPlayer: | |||
LDX !SpriteIndex | |||
%DamagePlayer() | |||
RTS | |||
</pre> | |||
==== Rebotar sobre el Sprite ==== | |||
Este comportamiento es muy común en los sprites de SMW, sin embargo no es sencillo de realizar debido a que cuando el sprite se mueve y player rebota sobre el sprite, la precisión de la interacción puede provocar muchos problemas, en varias ocasiones el player no rebotara y podría ser dañado siendo que a ojos del usuario, si debido haber rebotado. Debido a esto, explicare paso a paso, todo lo que se requiere para hacer esta rutina 100% precisa, adaptándose tanto a múltiples cajas de colisión como adaptándose a cualquier velocidad del sprite o el player. | |||
Para esto necesitaremos definir los siguientes defines: | |||
<pre> | |||
!PlayerHitboxBottomYLowByte = !SpriteMiscTable10 | |||
!PlayerHitboxBottomYHighByte = !SpriteMiscTable11 | |||
!PlayerIsAboveSprite = !SpriteMiscTable12 | |||
</pre> | |||
La parte compleja de este comportamiento es definir si debe o no rebotar, este chequeo lo dividiremos en: | |||
# ¿Cómo detectar si el player esta encima del sprite? | |||
# Si el player esta encima del sprite, ¿debe o no rebotar? | |||
# ¿Cómo se hace rebotar? | |||
'''¿Cómo detectar si el player esta encima del sprite?''' | |||
Para este debemos considerar la posición Y de la parte debajo la caja de colisión del player. Para esto calcularemos la posición Y de la caja y le sumamos el alto, tambien le restaremos 8 pixeles debido a que necesitamos un margen de seguridad, esto lo guardaremos en nuestras variables <code>!PlayerHitboxBottomYLowByte,x</code> y <code>!PlayerHitboxBottomYHighByte,x</code>: | |||
<pre> | |||
UpdatesPlayerHitboxBottom: | |||
JSR CalculatePlayerHitboxBottom | |||
STA !PlayerHitboxBottomYLowByte,x ;Updates Player Hitbox Bottom | |||
XBA | |||
STA !PlayerHitboxBottomYHighByte,x | |||
RTS | |||
CalculatePlayerHitboxBottom: | |||
LDA $03 | |||
STA $45 | |||
STZ $46 ;Load Hitbox height in 16 bits on the Scratch RAM $45 | |||
LDA $09 | |||
XBA | |||
LDA $01 ;A 16 bits = position Y of the hitbox | |||
REP #$20 | |||
CLC | |||
ADC $45 ;A 16 bits = position Y of the hitbox + hitbox height | |||
SEC | |||
SBC #$0008 ;Safety Range | |||
SEP #$20 | |||
RTS | |||
</pre> | |||
Debemos llamar la rutina <code>UpdatesPlayerHitboxBottom</code> justo después de la rutina de interacción con el player. Si quieres ahorrar un poco de espacio también puedes poner ambas rutinas en archivos separados y ponerlos en la carpeta routines de pixi, solo tendrias que cambiar ambos RTS por RTL. | |||
Ahora una vez calculado esto, haremos ciertas rutinas que nos ayuden a saber detectar si el player esta o no arriba. | |||
<pre> | |||
;Uses CheckIfIsAbove with latest Player Hitbox Bottom Y position. | |||
CheckIfPlayerWasAbove: | |||
LDA !PlayerHitboxBottomYHighByte,x | |||
XBA | |||
LDA !PlayerHitboxBottomYLowByte,x | |||
JSR CheckIfIsAbove | |||
RTS | |||
;Uses CheckIfIsAbove with current Player Hitbox Bottom Y position. | |||
CheckIfPlayerIsAbove: | |||
JSR CalculatePlayerHitboxBottom | |||
JSR CheckIfIsAbove | |||
RTS | |||
;Checks Sprite Hitbox Top with the value in A register (16 bits) | |||
;Return Carry Clear if is above, set if not. | |||
CheckIfIsAbove: | |||
REP #$20 | |||
STA $47 | |||
SEP #$20 | |||
LDA $0B | |||
XBA | |||
LDA $05 ;Load sprite hitbox top | |||
REP #$20 | |||
CMP $47 | |||
SEP #$20 ;Compare changing Carry flag. | |||
RTS | |||
</pre> | |||
Con estas rutinas sabremos si el player estaba arriba o no de la caja de colisión cuando se detecte esta colisión. Si en el Game Loop (SNES Frame) actual luego de usar el comando <code>JSR CheckIfPlayerIsAbove</code> nos da que el Carry es 0 (Carry Clear), es por que el player esta encima del sprite, ahora si que el Carry es 1 (Carry Set), debemos ahora verificar si en el frame anterior estaba encima, ya que si en el frame anterior estaba encima y ahora esta por debajo, entonces hubo un error de precisión y el player si esta encima del sprite, para crearemos esta rutina que le llamaremos '''MustPlayerBeingAbove''': | |||
<pre> | |||
PlayerMustBeAbove: | |||
JSR CheckIfPlayerIsAbove | |||
BCC + | |||
JSR CheckIfPlayerWasAbove | |||
+ | |||
RTS | |||
</pre> | |||
Esta rutina basicamente haria lo siguiente: | |||
{| class="wikitable" | |||
|- | |||
! !! El player esta encima en el ciclo actual !! El player no esta encima en el ciclo actual | |||
|- | |||
| El player esta encima en el ciclo anterior || El player esta encima del sprite (Carry Clear) || El player esta encima del sprite (Carry Clear) | |||
|- | |||
| El player no esta encima en el ciclo actual|| El player esta encima del sprite (Carry Clear) || El player no esta encima del sprite (Carry Set) | |||
|} | |||
'''Si el player esta encima del sprite, ¿debe o no rebotar?''' | |||
Una ves sabemos que el sprite esta encima, debemos realizar los siguientes chequeos: | |||
* Revisar si el player esta tocando el piso, ya que, si el player esta tocando el piso, no es correcto que rebote. | |||
* Revisar si la velocidad relativa entre el sprite y el player es tal, que el player efectivamente este pisando al sprite. | |||
El primer chequeo es sencillo, podemos realizarlo con las siguientes 2 lineas de codigo: | |||
<pre> | |||
LDA !PlayerBlockedStatus_S00MUDLR | |||
AND #$04 | |||
</pre> | |||
Si luego de usar esas 2 lineas, el valor de A es 0, entonces no esta tocando el piso, en otro casi si esta tocando piso. | |||
El chequeo complicado es el de velocidad relativa, basicamente se divide primero en obtener la velocidad real en Y y luego compararla con la velocidad del player. Para el primer paso utilizaremos la siguiente rutina: | |||
<pre> | |||
LDA !SpriteBlockedStatus_ASB0UDLR,x | |||
AND #$24 | |||
BNE ?+ | |||
RTL | |||
?+ | |||
LDA !SpriteXSpeed,x | |||
ROL | |||
ROL | |||
AND #$01 | |||
STA !Scratch45 | |||
LDA $15B8|!addr,x | |||
CLC | |||
ADC #$04 | |||
ASL | |||
ORA !Scratch45 | |||
ASL | |||
PHA | |||
LDA !SpriteXSpeed,x | |||
BPL ?+ | |||
EOR #$FF | |||
INC A | |||
?+ | |||
PLX | |||
JSR (?+++,x) | |||
RTL | |||
?+++ | |||
dw ?++++ ;Right X Speed, Very steep slope left. | |||
dw ?+++++ ;Left X Speed, Very steep slope left. | |||
dw ?++++++ ;Right X Speed, Steep slope left. | |||
dw ?+++++++ ;Left X Speed, Steep slope left. | |||
dw ?++++++++ ;Right X Speed, Normal slope left. | |||
dw ?+++++++++ ;Left X Speed, Normal slope left. | |||
dw ?++++++++++ ;Right X Speed, Gradual slope left. | |||
dw ?+++++++++++ ;Left X Speed, Gradual slope left. | |||
dw ?++++++++++++ ;No Slope | |||
dw ?++++++++++++ ;No Slope | |||
dw ?+++++++++++ ;Right X Speed, Gradual slope Right. | |||
dw ?++++++++++ ;Left X Speed, Gradual slope Right. | |||
dw ?+++++++++ ;Right X Speed, Normal slope Right. | |||
dw ?++++++++ ;Left X Speed, Normal slope Right. | |||
dw ?+++++++ ;Right X Speed, Steep slope Right. | |||
dw ?++++++ ;Left X Speed, Steep slope Right. | |||
dw ?+++++ ;Right X Speed, Very steep slope Right. | |||
dw ?++++ ;Left X Speed, Very steep slope Right. | |||
?++++ ;Right X Speed, Very steep slope left. | |||
LDX !SpriteIndex | |||
CLC | |||
ASL | |||
BPL ?+ | |||
LDA #$7F | |||
?+ | |||
EOR #$FF | |||
INC A | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?+++++ ;Left X Speed, Very steep slope left. | |||
LDX !SpriteIndex | |||
CLC | |||
ASL | |||
BPL ?+ | |||
LDA #$7F | |||
?+ | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?++++++ ;Right X Speed, Steep slope left. | |||
LDX !SpriteIndex | |||
EOR #$FF | |||
INC A | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?+++++++ ;Left X Speed, Steep slope left. | |||
LDX !SpriteIndex | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?++++++++ ;Right X Speed, Normal slope left. | |||
LDX !SpriteIndex | |||
LSR | |||
EOR #$FF | |||
INC A | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?+++++++++ ;Left X Speed, Normal slope left. | |||
LDX !SpriteIndex | |||
LSR | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?++++++++++ ;Right X Speed, Gradual slope left. | |||
LDX !SpriteIndex | |||
LSR | |||
LSR | |||
EOR #$FF | |||
INC A | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?+++++++++++ ;Left X Speed, Gradual slope left. | |||
LDX !SpriteIndex | |||
LSR | |||
LSR | |||
STA !SpriteYSpeed,x | |||
RTS | |||
?++++++++++++ ;No Slope | |||
LDX !SpriteIndex | |||
STZ !SpriteYSpeed,x | |||
RTS | |||
</pre> | |||
Esta rutina es algo complicada, pero básicamente actualiza el valor de la velocidad Y del sprite dependiendo de si esta en un slope o no. Esta rutina pueden incluirla en la carpeta routines de pixi con el nombre '''"GetRealSpriteYSpeed"'''. | |||
Luego, para saber si debe o no rebotar, la velocidad del player debe ser mayor (considerando el signo) que la del sprite, de la siguiente manera: | |||
<pre> | |||
LDA !PlayerYSpeed | |||
CMP !SpriteYSpeed,x | |||
</pre> | |||
Si luego de esas 2 líneas de código, el '''flag N''' (BPL salta si es 1 y BMI salta si es 0), es 1, significa que el player debe rebotar. Por lo tanto la rutina completa deberia ser: | |||
<pre> | |||
;Return Carry Clear if can bounce, Carry set if not | |||
CanBounce: | |||
LDA !PlayerBlockedStatus_S00MUDLR | |||
AND #$04 | |||
BNE ?+ | |||
%GetRealSpriteYSpeed() | |||
LDA !PlayerYSpeed | |||
CMP !SpriteYSpeed,x | |||
BPL ?+ | |||
SEC | |||
RTL | |||
?+ | |||
CLC | |||
RTL | |||
</pre> | |||
Recomiendo guardar este resultado en alguna scratch que puedas revisar desde los codigos de las cajas de colisión. | |||
'''¿Cómo se hace rebotar?''' | |||
Una ves que se cumplen las condiciones debemos hacer que el player rebote, para esto podemos simplemente llamar estas 2 rutinas: | |||
<pre> | |||
JSL $01AB99|!rom ;Display White Star | |||
JSL $01AA33|!rom ;Do the player boost its Y Speed | |||
</pre> | |||
Sin embargo no basta con esto, también debemos antes de llamar estas rutinas, modificar la posición del player para que este just arriba de la caja, para esto restaremos la posición en el tope de la caja con la posición del la parte de abajo de la caja de colisión del player y le sumaremos el resultado a la posición del player. | |||
<pre> | |||
GetDeltaPlayerHitboxBottomAndSpriteHitboxTop: | |||
LDA $03 | |||
STA $45 | |||
STZ $46 ;$45 = player hitbox height | |||
LDA $09 | |||
XBA | |||
LDA $01 | |||
REP #$20 | |||
CLC | |||
ADC $45 | |||
STA $45 | |||
SEP #$20 ;$45 = Player Hitbox Bottom | |||
LDA $0B | |||
XBA | |||
LDA $01 ;A 16 bits = Sprite hitbox top | |||
REP #$20 | |||
SEC | |||
SBC $45 ;A 16 bits = Sprite hitbox top - Player Hitbox Bottom | |||
EOR #$FFFF | |||
INC A ;A 16 bits = Player Hitbox Bottom - Sprite hitbox top | |||
RTS | |||
</pre> | |||
Esta rutina calculara esa diferencia, ahora debemos usar eso para sumarselo a la posición del player: | |||
<pre> | |||
JSR GetDeltaPlayerHitboxBottomAndSpriteHitboxTop | |||
CLC | |||
ADC !PlayerY | |||
STA !PlayerY | |||
SEP #$20 | |||
</pre> | |||
Esta rutina, calcularia el delta entre ambas posiciones y luego se lo sumaria a la posición del player, ahora debemos además sumarle ese valor a la posición de la caja de colisión, por lo que quedaría asi: | |||
<pre> | |||
UpdatePlayerPositionAfterBounce: | |||
JSR GetDeltaPlayerHitboxBottomAndSpriteHitboxTop | |||
PHA | |||
CLC | |||
ADC !PlayerY | |||
STA !PlayerY | |||
SEP #$20 | |||
LDA $09 | |||
XBA | |||
LDA $01 | |||
REP #$20 | |||
STA $45 | |||
PLA | |||
CLC | |||
ADC $45 | |||
SEP #$20 | |||
STA $01 | |||
XBA | |||
STA $09 | |||
RTS | |||
</pre> | |||
Cuando una caja de colisión defina que el player debe rebotar, entonces pondremos la variable <code>!PlayerIsAboveSprite,x</code> en el valor 2. Además actualizaremos la posición del player para que este encima de la caja. | |||
==== Interacción Solida ==== | |||
== Creación de Clusters y Extended Sprites == | == Creación de Clusters y Extended Sprites == | ||
== Sprites Dinamicos == | == Sprites Dinamicos == |
Latest revision as of 23:57, 30 March 2021
English | Português | Español | 日本語 |
En el siguiente tutorial se explicara como crear Sprites utilizando Dyzen. Este tutorial cubre no solo el como utilizar el tool, sino que además, cubre diversos comportamientos recurrentes en la creación de sprites normales, clusters y extended.
Para este tutorial se presume que el desarrollador tiene conocimientos previos de ASM como el uso de comandos básicos, branching e indexación. Este tutorial esta basado más en como construir la lógica de un sprite.
También debe notarse que este tutorial también puede servir para sprites que no son creados con Dyzen aunque el foco este basado en este ultimo.
Creando un CFG/JSON con CFG Editor
Estructura de un sprite
Un sprite se estructura de la siguiente manera:
-
Sprite Init: Es la rutina que ocurre cuando el sprite es creado. En Pixi esta rutina inicia con la linea:
print "INIT ",pc
Y debe terminar con un RTL.
-
Sprite Main: Esta rutina es llamada en cada Game Loop (SNES Frame) y se utiliza para actualizar al sprite y su logica. En pixi esta rutina inicia con la linea:
print "MAIN ",pc
Y debe debe terminar con un RTL. Usualmente esta rutina sera de esta manera:
print "MAIN ",pc PHB PHK PLB JSR SpriteCode PLB RTL
Llamando a una rutina llamada SpriteCode que sera el contenido real de esta rutina. Se realiza esto con el fin de mantener un mejor orden en el codigo y además setear el Program Bank para que se puedan indexar las tablas con indexación corta ($XXXX,x o $XXXX,y), en ves de usar indexación larga y asi disminuir la cantidad de ciclos en el codigo.
- SpriteCode: Es basicamente el contenido de la rutina Main previamente descrita. Esta rutina llamara a otras que haran la logica del sprite, separaremos estas rutinas en DynamicRoutine, GraphicRoutine, InteractionWithPlayer, InteractionWithSprites, StateMachine, AnimationRoutine, entre otras, dependiendo de que requiera el sprite. Una estructura basica de esta rutina es:
SpriteCode: .AlwaysExecutedZone JSR GraphicRoutine ;Calls the graphic routine and updates sprite graphics ;Here you can put code that will be excecuted each frame even if the sprite is locked LDA !SpriteStatus,x CMP #$08 ;if sprite dead return BNE Return LDA !LockAnimationFlag BNE Return ;if locked animation return. .ExecutedIfNotLocked %SubOffScreen() JSR InteractMarioSprite ;After this routine, if the sprite interact with mario, Carry is Set. ;Here you can write your sprite code routine ;This will be excecuted once per frame excepts when ;the animation is locked or when sprite status is not #$08 JSR AnimationRoutine ;Calls animation routine and decides the next frame to draw RTS Return: RTS
Donde podemos notar 2 zonas principales:
- .AlwaysExecutedZone que comprende el sector desde el inicio de la rutina hasta el primer chequeo
LDA !SpriteStatus,x
. Esta zona ocurre siempre, incluso si el juego esta en pausa o las animaciones bloqueadas, normalmente no pondremos mucho codigo en esta zona salvo por la rutina gráfica. - .ExecutedIfNotLocked que comprende el sector despues del chequeo
LDA !LockAnimationFlag
, esta zona ocurre cuando las animaciones no estan bloqueadas, o sea cuando el juego no esta pausado o el player no esta en animación de muerte o similares, tambien. Tambien debe aclararse que este sector solo es ejecutado si el!SpriteStatus,x
tiene el valor 8, esto significa que el el sprite esta con el status "Rutina Normal", esta dirección de RAM corresponde con la tabla $14C8, cabe destacar que modificando el chequeo del!SpriteStatus,x
se puede hacer que el sprite ejecute el codigo en otros status, siendo uno de los más comunes el 2 que ocurre cuando el sprite esta muriendo. En este sector podemos notar que se llaman a otras rutinas que iremos detallando a lo largo del tutorial, lo importante que debemos saber aca es que debajo de la del llamadoJSR InteractMarioSprite
es donde ejecutaremos el codigo de la lógica del sprite.
- .AlwaysExecutedZone que comprende el sector desde el inicio de la rutina hasta el primer chequeo
Maquinas de Estado
Una Maquina de estados esta conformada por 2 partes principales, los estados y las transiciones entre estados. En estas maquinas, se ejecuta solo un estado al mismo tiempo y este cambia según las condiciones lógicas implementadas a los estados que son alcanzables desde este.
No se planea hacer un curso completo sobre maquinas de estados, ya que, esta materia puede tomar semestres de estudios para profundizar todas sus distintas dimensiones, sin embargo, utilizaremos este concepto para armar la lógica de nuestro sprite.
En este esquema la idea es tener una rutina central que llamaremos StateMachine
que sera utilizada para llamar a la rutina correcta según el estado que es ejecutado. Para esto necesitaremos una tabla de sprites que usaremos con este proposito, se recomienda sobretodo una tabla misc, ya que, asi no necesitamos buscar freerams que pueden terminar siendo utilizadas para otros recursos, para esto podemos dirigirnos al tope del codigo y en la zona demarcada con los comentarios:
;###################################### ;############## Defines ############### ;######################################
Podemos agregar la variable:
!State = !SpriteMiscTable8
Utilizando una de las tablas Misc. de sprites que pueden ser usadas para cualquier proposito que desees.
Una vez hecho lo anterior necesitaremos poner esta variable con el valor 0, usaremos el estado 0 para nuestro estado inicial, ahora no siempre podemos iniciarlo en 0, podríamos por ejemplo, hacer que a través del extra byte o el extra property, darle el estado inicial al sprite, con el fin de generar distintas versiones del mismo sprite, pero lo más usual es usar el estado 0 como estado inicial. Para esto en el SpriteInit, agregaremos la linea:
STZ !State,x ;!State,x = 0
Luego de esto crearemos una rutina llamada "StateMachine" que ejecutara una rutina distinta dependiendo de la variable !State,x
. Esta rutina, sera la siguiente:
StateMachine: LDA !State,x ;A Reg = !State,x ASL TAX ;Transform A Reg into a index value for table States and put that value in the X Reg JSR (States,x) ;Call Routine on the table States using X reg value. RTS States: dw State0 dw State1 dw State2 . . .
Podemos notar que esta rutina utiliza una tabla llamada "States", en esta tabla pondremos todos los estados que tenga nuestro sprite como correr, estar detenido, voltear, morir, etc. No es necesario ponerle de nombre a los estados "State0", simplemente usa el nombre que veas más conveniente, ahora normalmente se recomienda enumerarlos, ya que, sirve para recordar que valor de la variable !State,x
llama a cada estado.
Para aquellos que conocen algun lenguaje de programación de alto nivel, esta rutina seria el equivalente a:
switch(state) { case 0: state0(); break; case 1: state1(); break; case 2: state2(); break; case 3: state3(); break; etc... }
Esta rutina la pondremos en cualquier sector debajo de la rutina "SpriteCode", solo intenta ser ordenado, ya que, en ASM es muy fácil que el código quede hecho un desastre. Una vez creada esta rutina, la llamaremos en la zona de la rutina "SpriteCode" que sucede antes del JSR AnimationRoutine
. Sin embargo, en este momento si probamos el sprite ahora mismo, no va a poder ser insertado, ya que, los labels de los estados no han sido creado asi que debemos crear cada uno de los estados.
Estructura de un Estado
Para crear cada uno de los estados seguiremos la siguiente base:
StateX: LDX !SpriteIndex ;Load Sprite index on X Register LDA !AnimationIndex,x CMP #$XX ;Index of the animation represented by the State BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_XXXX ;Change the animation to the animation with index #$XX RTS .StateLoop RTS
Como podemos notar, esta base tiene 2 secciones:
- .StateStart que es llamada cuando el sprite cambia desde otro estado al nuevo estado.
- .StateLoop que es llamada cuando en cada ciclo despues que el estado ya fue iniciado.
Debemos notar que esta base de estados estaría linkeada a una y solo una animación al mismo tiempo, obviamente, se puede hacer que un estado usara más de una animación haciendo modificaciones a esta estructura, pero en la practica hacer esto no suele ser muy necesario y tiene la desventaja que genera un código más desordenado y difícil de leer, además, complica la lógica del estado, por esto, recomiendo usar 1 animación y solo una por estado a menos que sea realmente muy necesario.
Algo que podemos notar de esta estructura, es que desde fuera del estado podemos llamar tanto el "StateStart" como el "StartLoop" usando los comandos JSR StateX_StateStart
y JSR StateX_StateLoop
, llamar al "StateStart" puede ser útil en ciertas circunstancias donde hacemos algunas transiciones donde si o si necesitamos iniciar el estado de manera externa, esto es común sobre todo en sprites dinámicos o en animaciones de volteo, ahora llamar al "StateLoop" no es tan útil, rara vez podríamos encontrar una razón para llamar esta sección de manera externa a menos que tuviéramos otro estado que fuera casi idéntico pero con una pequeña variación.
Otro detalle importante es notar que el estado al inicio tiene un LDX !SpriteIndex
, esto se debe a que los sprites utilizan tablas para sus variables y estas tablas usan el índice del Sprite almacenado en el registro X para podes acceder al valor correcto de la tabla, sin embargo, cuando llamamos la rutina "StateMachine", el valor del registro X fue modificado y perdimos el índice del sprite en el registro X, así que usamos este comando para restaurarlo.
Por ultimo, cabe la duda ¿Cómo se el índice de la animación utilizada en el estado?, esta se responde yendo a la sección del código donde están las rutinas ChangeAnimationFromStart
, ahí podremos encontrar algo como esto:
ChangeAnimationFromStart_Walk: STZ !AnimationIndex,x JMP ChangeAnimationFromStart ChangeAnimationFromStart_Flip: LDA #$01 STA !AnimationIndex,x JMP ChangeAnimationFromStart ChangeAnimationFromStart_Resist: LDA #$02 STA !AnimationIndex,x JMP ChangeAnimationFromStart ChangeAnimationFromStart_DeathLoop: LDA #$03 STA !AnimationIndex,x JMP ChangeAnimationFromStart ChangeAnimationFromStart_Death: LDA #$04 STA !AnimationIndex,x
Donde el valor cargado en A seria el índice de la animación correspondiente, en este ejemplo:
Walk => !AnimationIndex,x = 0 Flip => !AnimationIndex,x = 1 Resist => !AnimationIndex,x = 2 DeathLoop => !AnimationIndex,x = 3 Death => !AnimationIndex,x = 4
Condiciones y Transiciones
Las transiciones permiten que bajo ciertas condiciones el estado cambie a otro, cada estado podrá cambiar a un número limitado de otros estados con el fin que el sprite tenga el comportamiento deseado. Por ejemplo, si tuviéramos un sprite que simplemente se moviera de un lado a otro sin detectar precipicios, necesitaríamos una maquina de esta manera:
Tendríamos un sprite que caminaría en linea recta hasta toparse con una muralla, luego cambiaria al estado de volteo reproduciendo esa animación y cuando termine la animación de volteo entonces volvería al estado caminar yendo hacia el otro lado. Y en ASM esa maquina se veria de esta manera:
StateMachine: LDA !State,x ;A Reg = !State,x ASL TAX ;Transform A Reg into a index value for table States and put that value in the X Reg JSR (States,x) ;Call Routine on the table States using X reg value. RTS States: dw Walk0 dw Flip1 Walk0: LDX !SpriteIndex ;Load Sprite index on X Register LDA !AnimationIndex,x CMP #$00 ;Index of the animation represented by the State BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Walk ;Change the animation to the animation with index #$XX LDA !GlobalFlip,x BEQ .right ;Check if is sprite direction is right or left LDA #$E0 ;If sprite direction is left then set negative X Speed BRA ++ .right LDA #$20 ;If sprite direction is right then set positive X speed ++ STA !SpriteXSpeed,x JSL $01802A|!rom ;Update Sprite position with gravity RTS .StateLoop JSL $01802A|!rom ;Update Sprite position with gravity LDA !SpriteBlockedStatus_ASB0UDLR,x AND #$03 BEQ + ;Check if Left or Right wall are blocked LDA #$01 STA !State,x ;If left or right are blocked then change to state flip + RTS Flip1: LDX !SpriteIndex ;Load Sprite index on X Register LDA !AnimationIndex,x CMP #$01 ;Index of the animation represented by the State BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Flip ;Change the animation to the animation with index #$XX STZ !SpriteXSpeed,x ;X Speed = 0 JSL $01802A|!rom ;Update Sprite position with gravity RTS .StateLoop JSL $01802A|!rom ;Update Sprite position with gravity LDA !AnimationFrameIndex,x CMP #$XX BCC + ;Checks the last frame of the animation LDA !AnimationTimer,x BEQ + ;Check if the frame finished LDA !GlobalFlip,x ;If animation finish EOR #$01 ; STA !GlobalFlip,x ;Alternate sprite direction STZ !State,x ;State = Walk JSR Walk0_StateStart ;Call Walk0 State Start + RTS
Analicemos el código. Primero podemos notar que tiene 2 estados como se muestra en el diagrama, el primero es Walk con índice 0 y el segundo es Flip con indice 1. Iniciemos viendo el estado Walk0, este tiene en el "StateStart" la siguiente condición:
LDA !GlobalFlip,x BEQ .right ;Check if is sprite direction is right or left LDA #$E0 ;If sprite direction is left then set negative X Speed BRA ++ .right LDA #$20 ;If sprite direction is right then set positive X speed ++ STA !SpriteXSpeed,x
Esta condición primero revisa la variable !GlobalFlip,x
que tiene el valor 0 si el sprite mira hacia la derecha o 1 si el sprite mira hacia la izquierda (puede ser al reves dependiendo de como se hizo el sprite en Dyzen), entonces si el sprite mira hacia la derecha (!GlobalFlip,x es 0
) entonces carga en el registro A un valor positivo y en caso contrario carga en el registro A un valor negativo, luego de esto llama STA !SpriteXSpeed,x
que setearia la velocidad del sprite y luego refresca la posición en pantalla del sprite con JSL $01802A|!rom
.
Luego en la sección "StateLoop" podemos notar el siguiente chequeo:
LDA !SpriteBlockedStatus_ASB0UDLR,x AND #$43 BEQ + ;Check if Left or Right wall are blocked LDA #$01 STA !State,x ;If left or right are blocked then change to state flip +
Este chequeo revisaría los flags de la variable !SpriteBlockedStatus_ASB0UDLR,x
, donde:
- A: Flag que es 1 si el sprite esta tocando un bloque solido por encima en el Layer 2 (Detección de piso).
- S: Flag que es 1 si el sprite esta tocando un bloque solido por los lados en el Layer 2 (Detección de paredes).
- B: Flag que es 1 si el sprite esta tocando un bloque solido por abajo en el Layer 2 (Detección de techo).
- U: Flag que es 1 si el sprite esta tocando un bloque solido por arriba en el Layer 1 (Detección de techo).
- D: Flag que es 1 si el sprite esta tocando un bloque solido por abajo en el Layer 1 (Detección de piso).
- R: Flag que es 1 si el sprite esta tocando un bloque solido por la derecha en el Layer 1 (Detección de pared derecha).
- L: Flag que es 1 si el sprite esta tocando un bloque solido por la izquierda en el Layer 1 (Detección de pared izquierda).
Usamos el comando AND
para que todos los flags que no necesitamos queden en 0, en este caso estamos detectando paredes asi que los flags A, B, U y D no los necesitamos, por eso se usa el valor #$43
que en binario seria #$01000011
. De esta manera si el registro A luego de usar el and es distinto de 0 entonces esta tocando una pared. Luego de esto cambiaria el valor de !State,x
para que use el estado Flip.
Se debe destacar en este caso, que la rutina JSL $01802A|!rom
actualiza el valor de !SpriteBlockedStatus_ASB0UDLR,x
, por este motivo es llamada al inicio del "StateLoop" y no al final.
Ahora revisaremos el estado Flip. Lo primero que debemos notar es que este también actualiza la posición del sprite, un incauto podría pensar que no necesita actualizar la posición durante la rutina Flip, ya que, el sprite no deberia moverse cuando esta tocando una muralla, sin embargo esto no es cierto y se debe a que se debe cubrir el caso en que el sprite no esta tocando el piso, por lo tanto, la posición vertical si debe ser actualizada.
El "StateStart" en este caso es muy simple, solamente pone la velocidad horizontal en 0, lo interesante en este estado ocurre en el "StateLoop", aca podemos notar los siguientes chequeos:
LDA !AnimationFrameIndex,x CMP #$XX BCC + ;Checks the last frame of the animation LDA !AnimationTimer,x BEQ + ;Check if the frame finished LDA !GlobalFlip,x ;If animation finish EOR #$01 ; STA !GlobalFlip,x ;Alternate sprite direction STZ !State,x ;State = Walk JSR Walk0_StateStart ;Call Walk0 State Start +
Iniciaremos con:
LDA !AnimationFrameIndex,x CMP #$XX BCC +
Este revisa la variable !AnimationFrameIndex,x
, esta variable, podemos utilizarla para saber que frame dentro de la animación, se esta reproduciendo, por lo tanto, la idea seria revisar si el frame que se reproduce es el ultimo frame de la animación. ¿Cómo podemos saber que valor poner en #$XX? pues iremos a la tabla de esa animación, en la rutina de animación hay una tabla llamada "Frames", ejemplo:
Frames: Animation0_Walk_Frames: db $00,$01,$02,$03,$04,$05,$06,$07 Animation1_Flip_Frames: db $08,$08 Animation2_Resist_Frames: db $09 Animation3_DeathLoop_Frames: db $0A,$0B Animation4_Death_Frames: db $0C,$0D,$0E,$0F,$10
En este ejemplo, podemos notar que la animación de Flip tiene solo 2 frames, entonces el valor que tendríamos que poner es #$01, (Tamaño de la animación - 1).
Otro método para saber que número poner es ir a la tabla "AnimationLenght" que tiene el tamaño de cada animación, ejemplo:
AnimationLenght: dw $0008,$0002,$0001,$0002,$0005
Podemos notar que en esta tabla se muestra el tamaño de cada una de las animaciones, en este caso:
Walk => Tamaño 8 frames, ultimo frame #$07 Flip => Tamaño 2 frames, ultimo frame #$01 Resist => Tamaño 1 frames, ultimo frame #$00 DeathLoop => Tamaño 2 frames, ultimo frame #$01 Death => Tamaño 5 frames, ultimo frame #$04
Una vez entendido como funciona este chequeo, iremos al siguiente:
LDA !AnimationTimer,x BEQ +
Este es bastante simple, lo que indica la variable !AnimationTimer,x
es la cantidad de Game Loops (SNES Frames) para que el frame cambie, por lo tanto, si tiene un valor distinto de 0, significa que el frame aun se esta reproduciendo.
Luego de estos 2 checks usamos los comandos:
LDA !GlobalFlip,x ;If animation finish EOR #$01 ; STA !GlobalFlip,x ;Alternate sprite direction
El comando EOR
puede ser utilizado para alternar bits del registro A, por ejemplo, EOR #$01
alternaría el bit de más a la derecha, entonces si ese bit era un 0 lo cambiaria a un 1 y si era un 1, lo cambiaria a 0. usamos esto para alternar la dirección del sprite.
Por ultimo se pone se vuelve al estado Walk pero además se llama al "StateStart" del estado walk, la razón de esto es que al alternar la dirección del sprite, ocurriría que por 1 frame el sprite muestre la animación volteada incorrectamente, por esto, en los estados de volteo es muy común llamar el "StateStart".
Aca podemos notar el resultado de este comportamiento:
Movimiento
Movimiento Básico
Movimiento Avanzado
Interacción
Dyzen tiene su propio sistema de interacción, sin embargo, este no es obligatorio de usar, por lo que, se profundizara en las distintas maneras de realizar la interacción entre el Sprite y otras entidades del juego como puede ser el player, proyectiles, otros sprites, etc. En esta sección además veremos ciertas acciones que se pueden realizar cuando la colisión es detectada.
Detección de colisión con Player
Vanilla
Para detectar si el sprite interactua con el player, primero debemos en el CFG Editor seleccionar el Sprite Clipping que deseamos, este aparecera en el cuadrado azul debajo:
Debemos considerar que el cuadrado que celeste oscuro que sale seria equivalente a el cuadrado de borde rojo que sale en el centro en Dyzen:
Por lo tanto, cuando hagas el sprite en Dyzen debes considerar esto para poner los gráficos en la posición correcta para que calce con la Hitbox.
Si tu sprite tiene una interacción común y corriente, puede que no necesites hacer nada en el ASM, mientras mantengas la casilla "Don't use default interactión with Mario" en blanco. Pero si necesitas realizar algo fuera de lo común puedes usar los siguientes comandos:
JSL $03B664|!rom ;Load Player Hitbox/Clipping JSL $03B69F|!rom ;Load Sprite Hitbox/Clipping JSL $03B72B|!rom ;Check For contact between Player and Sprite BCC + .ThereIsContact ;Here put the code that happends when contact between Player and Sprite exists. +
Como podemos notar, primero se carga la caja de colisión del player, luego se carga la del sprite y luego se llama otra rutina para verificar si existe contacto entre ambos.
Custom
Para una rutina personalizada es practicamente lo mismo que en la rutina vanilla, lo que cambia es que debes cargar la caja de colisión del sprite manualmente, para esto podemos utilizar las siguientes direcciones de RAM:
- $00 (!Scratch0): Low byte de la posición X de la caja de colisión.
- $01 (!Scratch1): Low byte de la posición Y de la caja de colisión.
- $02 (!Scratch2): Ancho de la caja de colisión.
- $03 (!Scratch3): Alto de la caja de colisión.
- $08 (!Scratch8): High Byte de la posición X de la caja de colisión.
- $09 (!Scratch9): High Byte de la posición Y de la caja de colisión.
Por lo tanto la manera adecuada de usarlo seria algo como lo siguiente:
JSL $03B664|!rom ;Load Player Hitbox/Clipping LDA !SpriteXHigh,x ;Load Sprite X High Byte XBA ; LDA !SpriteXLow,x ;Load Sprite X Low Byte REP #$20 ;A 16 bits = X position CLC ADC #!OffsetX ;A = Sprite X position + Hitbox X Offset (Offset must be 16 bits) SEP #$20 STA !Scratch0 ;Store Hitbox X Low Byte XBA STA !Scratch8 ;Store Hitbox X Low Byte LDA !SpriteYHigh,x ;Load Sprite Y High Byte XBA ; LDA !SpriteYLow,x ;Load Sprite Y Low Byte REP #$20 ;A 16 bits = Y position CLC ADC #!OffsetY ;A = Sprite Y position + Hitbox Y Offset (Offset must be 16 bits) SEP #$20 STA !Scratch1 ;Store Hitbox Y Low Byte XBA STA !Scratch9 ;Store Hitbox Y Low Byte LDA #!Width STA !Scratch2 ;Store Hitbox Width LDA #!Height STA !Scratch3 ;Store Hitbox Height JSL $03B72B|!rom ;Check For contact between Player and Sprite BCC + .ThereIsContact ;Here put the code that happends when contact between Player and Sprite exists. +
Como podemos notar, es la misma rutina, solo que esta vez los parametros los entregamos de forma manual en ves de definirlos en el CFG. Basicamente se carga la posición de la hitbox (Hitbox Offset + Sprite Position) y se carga el ancho y alto de la caja.
Interacción con Dyzen
El sistema de Dyzen es una generalización de la interacción personalizada, solo que en ves de entregar los parametros de esa manera, hace un loop que verifica distintas cajas de colisión de una tabla. En el caso de este sistema cada caja de colisión tiene una acción que es definida en el tool en la sección "Interaction" de Dyzen:
Entonces esa acción es llamada cuando existe contacto entre la caja de colisión respectiva y el player.
Detección de colisión con Otros Sprites
Vanilla
Para detectar colisión sprite<->sprite, es muy similar a la colisión con player, lo que cambia es que ahora se deben revisar todos los slots de sprites para detectar esa colisión.
El código seria como el siguiente:
InteractionWithSprites: JSL $03B69F|!rom ;Load Sprite Hitbox/Clipping LDX #!MaxSprites-1 ;Start the loop from the end of the sprites table .loop CPX !SpriteIndex BEQ .next ;Skip loop if the Sprite index is the same than the current sprite JSL $03B6E5|!rom ;Load the other Sprite Hitbox/Clipping JSL $03B72B|!rom ;Check For contact between Player and Sprite BCC .next .ThereIsContact PHX ;Preserve X Register value TXY ;The index of the other sprite is saved in Y Register LDX !SpriteIndex ;Load Sprite index of the current sprite ;Here put the code that happends when contact between Player and Sprite exists. PLX ;Restore X register Value .next DEX BPL .loop LDX !SpriteIndex ;Restore Sprite Index
Como se puede ver, seria un Loop que que revisa cada uno de los sprites y revisa si existe colisión entre ambos. Se recomienda antes del JSL $03B6E5|!rom
hacer un check del !SpriteNumber
o del !CustomSpriteNumber
para que solo colisione con los sprites que deseas y no con absolutamente todos. Tambien recuerda poner un check de la variable !SpriteStatus
, ya que, puede que solo te interese detectar la colisión con sprites que no estan muertos.
Custom
Para hacer una hitbox personalizada, haremos lo mismo que en el caso anterior solo que esta vez en ves de llamar a JSL $03B69F|!rom
, codificaremos nosotros mismo la hitbox como se vio en Detección de colisión con Player/Custom.
InteractionWithSprites: LDA !SpriteXHigh,x ;Load Sprite X High Byte XBA ; LDA !SpriteXLow,x ;Load Sprite X Low Byte REP #$20 ;A 16 bits = X position CLC ADC #!OffsetX ;A = Sprite X position + Hitbox X Offset (Offset must be 16 bits) SEP #$20 STA !Scratch0 ;Store Hitbox X Low Byte XBA STA !Scratch8 ;Store Hitbox X Low Byte LDA !SpriteYHigh,x ;Load Sprite Y High Byte XBA ; LDA !SpriteYLow,x ;Load Sprite Y Low Byte REP #$20 ;A 16 bits = Y position CLC ADC #!OffsetY ;A = Sprite Y position + Hitbox Y Offset (Offset must be 16 bits) SEP #$20 STA !Scratch1 ;Store Hitbox Y Low Byte XBA STA !Scratch9 ;Store Hitbox Y Low Byte LDA #!Width STA !Scratch2 ;Store Hitbox Width LDA #!Height STA !Scratch3 ;Store Hitbox Height LDX #!MaxSprites-1 ;Start the loop from the end of the sprites table .loop CPX !SpriteIndex BEQ .next ;Skip loop if the Sprite index is the same than the current sprite JSL $03B6E5|!rom ;Load the other Sprite Hitbox/Clipping JSL $03B72B|!rom ;Check For contact between Player and Sprite BCC .next .ThereIsContact PHX ;Preserve X Register value TXY ;The index of the other sprite is saved in Y Register LDX !SpriteIndex ;Load Sprite index of the current sprite ;Here put the code that happends when contact between Player and Sprite exists. PLX ;Restore X register Value .next DEX BPL .loop LDX !SpriteIndex ;Restore Sprite Index
El problema de este método es que si otro tiene una rutina de colisión sprite<->sprite e intenta detectar a nuestro enemigo, esa colisión seguirá usando el Sprite Clipping Vanilla, por lo tanto, para evitar esto, le pondremos en el CFG "Don't Interact with other sprites".
Interacción con Dyzen
Rutina Gráfica
Trucos en la Rutina Gráfica
Animación
Trucos en la Rutina de Animación
Sonido y Música
Comportamientos Comunes
Comportamientos durante el Funcionamiento Normal
Voltear cuando detecta un Precipicio
Perseguir al Player
Saltar
Normalmente para realizar un salto tenemos una animación de salto, otra animación que ocurre mientras el sprite esta cayendo y una ultima que ocurre cuando el sprite aterriza en el piso. Por esto y siguiendo las ideas del capitulo de Maquinas de Estado, crearemos 3 estados:
- Jump: Es el estado que realiza el salto.
- Fall: Es el estado que comienza cuando el sprite esta cayendo.
- Arrive: Es el estado que comienza cuando el sprite llega al piso.
Para este ejemplo, asumiremos que las animaciones de salto son las con índice #$01
, #$02
y #$03
respectivamente. La animación de Jump y Arrive deberían ser Only Once, mientras que Fall deberia ser Continuous.
Primero se crea la maquina de estados con los 3 estados, ahora, además de estos estados debemos crear un Idle, asi que serian 4 estados, el idle lo utilizaremos para gatillar el salto.
StateMachine: LDA !State,x ;A Reg = !State,x ASL TAX ;Transform A Reg into a index value for table States and put that value in the X Reg JSR (States,x) ;Call Routine on the table States using X reg value. RTS States: dw Idle0 dw Jump1 dw Fall2 dw Arrive3
El estado Idle sera muy simple, solamente detectara si hay colisión con el piso y si es así, pasara al estado jump. Si leíste el capitulo de Maquinas de estados esto no debería ser un problema. El estado Idle seria el siguiente:
Idle0: LDX !SpriteIndex ;Load Sprite index on X Register JSL $01802A|!rom ;Update Sprite position with gravity LDA !AnimationIndex,x ;Index of the animation represented by the State BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Idle ;Change the animation to the animation with index #$XX RTS .StateLoop LDA !SpriteBlockedStatus_ASB0UDLR,x AND #$24 BEQ + ;Check if the sprite is touching the floor. LDA #$01 STA !State,x ;Change to state Jump + RTS
Ahora crearemos el estado de Jump. Este estado debe ponerle un valor negativo a la velocidad en Y al inicio y cambiar la animación a Jump, luego, cuando detecta que la velocidad en Y es 0 o positiva, cambiar al estado Fall, recuerda que en SMW Velocidad en Y negativa es ir hacia arriba.
Jump1: LDX !SpriteIndex ;Load Sprite index on X Register JSL $01802A|!rom ;Update Sprite position with gravity LDA !AnimationIndex,x ;Index of the animation represented by the State CMP #$01 BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Jump ;Change the animation to the animation with index #$XX LDA #$D0 STA !SpriteYSpeed,x ;Set Y Speed to a negative value RTS .StateLoop LDA !SpriteYSpeed,x BMI + ;Check if Y Speed is Positive or negative. LDA #$02 ;If Y Speed >= 0 then STA !State,x ;Change to State Fall + RTS
Luego el estado Fall, solo moveria al sprite y cuando detecta el piso cambia al estado Arrive, es muy similar al estado Idle0.
Fall2: LDX !SpriteIndex ;Load Sprite index on X Register JSL $01802A|!rom ;Update Sprite position with gravity LDA !AnimationIndex,x ;Index of the animation represented by the State CMP #$02 BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Fall ;Change the animation to the animation with index #$XX RTS .StateLoop LDA !SpriteBlockedStatus_ASB0UDLR,x AND #$24 BEQ + ;Check if the sprite is touching the floor. LDA #$03 STA !State,x ;Change to state Arrive + RTS
Por ultimo, el estado arrive debería detectar que la animación termino y cambiar a idle, esto tambien esta documentado en la sección de Maquinas de estado, se vería de la siguiente manera. Asumiremos que la animación de arrive tiene 3 frames asi que el ultimo frame sera el #$02
Arrive3: LDX !SpriteIndex ;Load Sprite index on X Register JSL $01802A|!rom ;Update Sprite position with gravity LDA !AnimationIndex,x CMP #$03 ;Index of the animation represented by the State BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Arrive ;Change the animation to the animation with index #$XX RTS .StateLoop LDA !AnimationFrameIndex,x CMP #$02 BCC + ;Checks the last frame of the animation LDA !AnimationTimer,x BEQ + ;Check if the frame finished STZ !State,x ;State = Idle + RTS
Esta maquina de estados, haria al sprite saltar apenas toque el piso teniendo 4 animaciones distintas (Idle, Jump, Fall y Arrive), puedes en el estado Idle incluir timers o condiciones para que salte solo cuando tu lo deseas.
Probablemente estés pensando, que sucede si salta y además se mueve en horizontal. Lo que haremos en este caso es en el estado Idle, poner la Velocidad X en 0, luego en el estado Jump además en el StateInit además de ponerle la velocidad en Y, pondremos la velocidad en X, luego de esto tenemos varias opciones que deben realizarse StateStart de Jump como de Fall cuando detecte una pared:
- Poner la velocidad X en 0 y cuando el sprite llegue al piso, voltearlo.
- Voltear el Sprite y poner la velocidad X en el mismo valor con el signo opuesto.
- Se pase a un estado que gestione esa interacción.
Todas las opciones son validas y dependerá de tu sprite cual elegir.
Daño y Muerte
Para hacer este comportamiento pensaremos en un sprite como de Donkey Kong Country, que cuando muere se eleva un poco y luego cae fuera de pantalla. Para esto necesitaremos 3 estados:
- Hurt: Es el estado que ocurre cuando el sprite recibe daño.
- Dead: Es el estado que ocurre cuando el sprite muere elevándose por el aire.
- DeadFinish: Es el estado que ocurre cuando el sprite llega a su punto más alto y empieza a caer.
Para este ejemplo, asumiremos que las animaciones de Hurt, Dead y DeadFinish son #$01
,#$02
y #$03
respectivamente.
Antes de empezar, necesitaremos una variable que almacene la cantidad de vida (HP o hitpoints) que le quedan al enemigo. Para esto, en la sección de defines podemos crear el siguiente define:
!Hitpoints = !SpriteMiscTable9
Luego en el Sprite Init podemos poner la cantidad de vida que tiene. Para esto podemos utilizar tanto una constante o usar un extra byte para el hp inicial, en mi caso usare una constante.
LDA #$03 STA !Hitpoints,x
Luego de esto, crearemos una maquina de estado como esta:
StateMachine: LDA !State,x ;A Reg = !State,x ASL TAX ;Transform A Reg into a index value for table States and put that value in the X Reg JSR (States,x) ;Call Routine on the table States using X reg value. RTS States: dw Idle0 dw Hurt1 dw Dead2 dw DeadFinish3
El estado de Idle es irrelevante para este comportamiento, ya que, normalmente gatillaremos el estado Hurt1 desde algún tipo de interacción con el player o con ciertos objetos del juego, asi que empezaremos con el estado Hurt:
Hurt1: LDX !SpriteIndex ;Load Sprite index on X Register LDA !Hitpoints,x CMP #$01 BCS + STZ !Hitpoints,x ;if Hitpoints = 1 then go to state dead and hp = 0. LDA #$02 STA !State,x JMP Dead2 + JSL $01802A|!rom ;Update Sprite position with gravity LDA !AnimationIndex,x ;Index of the animation represented by the State CMP #$01 BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Hurt ;Change the animation to the animation with index #$XX DEC !Hitpoints,x RTS .StateLoop LDA !AnimationFrameIndex,x CMP #$02 BCC + ;Checks the last frame of the animation LDA !AnimationTimer,x BEQ + ;Check if the frame finished STZ !State,x ;Return to state 0 (Idle) RTS
En este caso asumimos que la animación de daño tiene 3 frames, lo que hace este estado es:
- Si le queda 1 de hp al recibir daño, el sprite pasara al estado de muerte y los hitpoints pasaran a ser 0.
- Si al sprite aun le queda hp, lo disminuye en 1 y utiliza la animación de daño.
- Una vez que termine la animación de daño, pasa al estado Idle.
Para llegar a este estado, usando el método que desees para dañar al sprite (ya sea saltándole encima, lanzándole caparazones o cualquier otro), en esa interacción debes cambiar el estado al estado Hurt. En este ejemplo seria con:
LDA #$01 STA !State,x
Luego el estado de Dead seria como el siguiente:
Dead2: LDX !SpriteIndex ;Load Sprite index on X Register LDA !AnimationIndex,x ;Index of the animation represented by the State CMP #$02 BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_Dead ;Change the animation to the animation with index #$XX LDA #$D0 STA !SpriteYSpeed,x ;Set Y Speed as Negative LDA #$02 STA !SpriteStatus,x ;Set Sprite Status to 2 (dying) RTS .StateLoop LDA !SpriteYSpeed,x BMI + ;Check if Y Speed is Positive or negative. LDA #$03 ;If Y Speed >= 0 then STA !State,x ;Change to State DeadFinish + RTS
Basicamente pone la velocidad en Y en un valor negativo, pone el !SpriteStatus,x
en 2 (muerte), reproduciría la animación de muerte mientras sube y cuando la velocidad en Y se vuelve 0 o positiva, pasa al estado DeadFinish.
Debemos notar que este estado no utiliza la rutina de actualización de movimiento, esto se debe a que cuando el !SpriteStatus,x
es 2, el juego actualiza su posición de manera automatica.
Por ultimo, tendriamos el estado "DeadFinish" que simplemente es un estado que solo cambia de animación.
DeadFinish3: LDX !SpriteIndex ;Load Sprite index on X Register LDA !AnimationIndex,x ;Index of the animation represented by the State CMP #$03 BEQ .StateLoop .StateStart JSR ChangeAnimationFromStart_DeadFinish ;Change the animation to the animation with index #$XX RTS .StateLoop RTS
Esto permitiría una muerte como las que se usan en el Donkey Kong Country donde el enemigo tiene una animación cuando recibe daño (en caso de tener HP), otra animación cuando empieza a morir saltando hacia arriba y luego cuando cae fuera de la pantalla usa otra animación.
Haciendo variaciones en este comportamiento probablemente puedas crear muertes más simples o más complejas, pero ya dependería de la creatividad de cada uno.
Comportamientos durante la Interacción con el Player
En este capitulo se veran comportamientos comunes que ocurren cuando se detecta colisión entre el player y una hitbox del sprite, se recomienda revisar primero el capitulo de Interacción.
Algo que debe ser considerado en esta sección es que, las cajas de colisión estaran guardada en la Scratch Rams que van desde $00 a $0B.
Los datos de la caja del player estaran en:
- $00: Low Byte de la Posición X de la caja de colisión.
- $01: Low Byte de la Posición Y de la caja de colisión.
- $02: Ancho de la caja de colisión.
- $03: Alto de la caja de colisión.
- $08: High Byte de la Posición X de la caja de colisión.
- $09: High Byte de la Posición Y de la caja de colisión.
Mientras que los datos de la caja de colisión del sprite estarán en:
- $04: Low Byte de la Posición X de la caja de colisión.
- $05: Low Byte de la Posición Y de la caja de colisión.
- $06: Ancho de la caja de colisión.
- $07: Alto de la caja de colisión.
- $0A: High Byte de la Posición X de la caja de colisión.
- $0B: High Byte de la Posición Y de la caja de colisión.
Detección del Player
Esta es una idea que puede ayudarte para que el sprite cambie su comportamiento cuando el player esta en cierta posición con respecto al player. La idea es tener una caja de colisión y cuando esa caja es detectada, pasar a cierto estado. Por ejemplo, supongamos que el estado disparar (asumiremos que es el estado #$01
para este ejemplo, aunque puede ser cualquier estado realmente) es gatillado cuando el player esta en cierta área, podemos llamar a una rutina como la siguiente:
ShootTrigger: LDX !SpriteIndex ;Restore X register LDA #$01 STA !State,x ;change state to Shoot State. RTS
Esta función simple, haria que el sprite pase al estado de disparo cuando el sprite esta en cierta posición. Se recomienda para esto usar una rutina de interacción custom o la que viene en Dyzen.
Se debe notar que la linea LDX !SpriteIndex
, no es del todo necesaria dependiendo de tu sistema de interacción, pero el sistema de Dyzen, cuando usa múltiples cajas de colisión con distintas acciones, requiere esta linea, ya que, se pierde el el valor del registro X.
Dañar al Player
Para dañar al player, podrias pensar en solo usar la linea JSL $00F5B7|!rom
, sin embargo, esta tiene un problema, que cuando el player esta montado sobre yoshi, en ciertas ocasiones, en ves de hacer que yoshi se escape, hace que el player se achique, por lo tanto, te recomiendo las siguientes rutinas que puedes poner en la carpeta routines
de pixi en un archivo ".asm" con el nombre DamagePlayer.
PHX LDA $187A|!addr ;if the player is not riding yoshi then damage the player BEQ ?+ ;otherwise dismount yoshi JSR ?FindYoshi BCC ?+ JSR ?DismountYoshi PLX RTL ?+ JSL $00F5B7|!rom PLX RTL ?FindYoshi: LDX $18DF|!addr BEQ ?.crawlForYoshi DEX BRA ?.found ?.crawlForYoshi: LDX.w $1692|!addr ; Start Slot according to sprite data LDA.l $02A773|!rom,x SEC SBC #$FE ; spaces 2 reserved slots, have to interact with them too TAX ?.loop: LDA !SpriteNumber,x CMP #$35 BNE ?.continueLoop LDA !SpriteStatus,x BNE ?.found ?.continueLoop DEX BPL ?.loop ?.returnClear: CLC RTS ?.found SEC RTS ?DismountYoshi: LDA #$10 STA !SpriteDecTimer6,x LDA #$03 ; \ Play sound effect STA $1DFA|!addr ; / LDA #$13 ; \ Play sound effect STA $1DFC|!addr ; / LDA #$02 STA !SpriteMiscTable3,x STZ $187A|!addr LDA #$C0 STA !PlayerYSpeed STZ !PlayerXSpeed %SubHorzPos() LDA ?XSpeedDismountTable,y STA !SpriteXSpeed,X STZ !SpriteMiscTable10,x STZ !SpriteMiscTable6,X STZ $18AE|!addr STZ $0DC1|!addr LDA #$30 ; \ Mario invincible timer = #$30 STA $1497|!addr ; / JSR ?CODE_01EDCC RTS ; Return ?XSpeedDismountTable: db $E8,$18 ?CODE_01EDCC: LDY.B #$00 LDA !SpriteYLow,X SEC SBC ?YoshiOffset,Y STA !PlayerY STA $D3 LDA !SpriteYHigh,X SBC #$00 STA !PlayerY+$01 STA $D4 RTS ; Return ?YoshiOffset: db $04,$10
Esta rutina hara que el si yoshi existe en el nivel y el player lo esta montando, entonces, en ves de dañar al player, haga que yoshi se escape. Luego puedes usar una rutina como esta para dañar al player de forma correcta.
HurtPlayer: LDX !SpriteIndex %DamagePlayer() RTS
Rebotar sobre el Sprite
Este comportamiento es muy común en los sprites de SMW, sin embargo no es sencillo de realizar debido a que cuando el sprite se mueve y player rebota sobre el sprite, la precisión de la interacción puede provocar muchos problemas, en varias ocasiones el player no rebotara y podría ser dañado siendo que a ojos del usuario, si debido haber rebotado. Debido a esto, explicare paso a paso, todo lo que se requiere para hacer esta rutina 100% precisa, adaptándose tanto a múltiples cajas de colisión como adaptándose a cualquier velocidad del sprite o el player.
Para esto necesitaremos definir los siguientes defines:
!PlayerHitboxBottomYLowByte = !SpriteMiscTable10 !PlayerHitboxBottomYHighByte = !SpriteMiscTable11 !PlayerIsAboveSprite = !SpriteMiscTable12
La parte compleja de este comportamiento es definir si debe o no rebotar, este chequeo lo dividiremos en:
- ¿Cómo detectar si el player esta encima del sprite?
- Si el player esta encima del sprite, ¿debe o no rebotar?
- ¿Cómo se hace rebotar?
¿Cómo detectar si el player esta encima del sprite?
Para este debemos considerar la posición Y de la parte debajo la caja de colisión del player. Para esto calcularemos la posición Y de la caja y le sumamos el alto, tambien le restaremos 8 pixeles debido a que necesitamos un margen de seguridad, esto lo guardaremos en nuestras variables !PlayerHitboxBottomYLowByte,x
y !PlayerHitboxBottomYHighByte,x
:
UpdatesPlayerHitboxBottom: JSR CalculatePlayerHitboxBottom STA !PlayerHitboxBottomYLowByte,x ;Updates Player Hitbox Bottom XBA STA !PlayerHitboxBottomYHighByte,x RTS CalculatePlayerHitboxBottom: LDA $03 STA $45 STZ $46 ;Load Hitbox height in 16 bits on the Scratch RAM $45 LDA $09 XBA LDA $01 ;A 16 bits = position Y of the hitbox REP #$20 CLC ADC $45 ;A 16 bits = position Y of the hitbox + hitbox height SEC SBC #$0008 ;Safety Range SEP #$20 RTS
Debemos llamar la rutina UpdatesPlayerHitboxBottom
justo después de la rutina de interacción con el player. Si quieres ahorrar un poco de espacio también puedes poner ambas rutinas en archivos separados y ponerlos en la carpeta routines de pixi, solo tendrias que cambiar ambos RTS por RTL.
Ahora una vez calculado esto, haremos ciertas rutinas que nos ayuden a saber detectar si el player esta o no arriba.
;Uses CheckIfIsAbove with latest Player Hitbox Bottom Y position. CheckIfPlayerWasAbove: LDA !PlayerHitboxBottomYHighByte,x XBA LDA !PlayerHitboxBottomYLowByte,x JSR CheckIfIsAbove RTS ;Uses CheckIfIsAbove with current Player Hitbox Bottom Y position. CheckIfPlayerIsAbove: JSR CalculatePlayerHitboxBottom JSR CheckIfIsAbove RTS ;Checks Sprite Hitbox Top with the value in A register (16 bits) ;Return Carry Clear if is above, set if not. CheckIfIsAbove: REP #$20 STA $47 SEP #$20 LDA $0B XBA LDA $05 ;Load sprite hitbox top REP #$20 CMP $47 SEP #$20 ;Compare changing Carry flag. RTS
Con estas rutinas sabremos si el player estaba arriba o no de la caja de colisión cuando se detecte esta colisión. Si en el Game Loop (SNES Frame) actual luego de usar el comando JSR CheckIfPlayerIsAbove
nos da que el Carry es 0 (Carry Clear), es por que el player esta encima del sprite, ahora si que el Carry es 1 (Carry Set), debemos ahora verificar si en el frame anterior estaba encima, ya que si en el frame anterior estaba encima y ahora esta por debajo, entonces hubo un error de precisión y el player si esta encima del sprite, para crearemos esta rutina que le llamaremos MustPlayerBeingAbove:
PlayerMustBeAbove: JSR CheckIfPlayerIsAbove BCC + JSR CheckIfPlayerWasAbove + RTS
Esta rutina basicamente haria lo siguiente:
El player esta encima en el ciclo actual | El player no esta encima en el ciclo actual | |
---|---|---|
El player esta encima en el ciclo anterior | El player esta encima del sprite (Carry Clear) | El player esta encima del sprite (Carry Clear) |
El player no esta encima en el ciclo actual | El player esta encima del sprite (Carry Clear) | El player no esta encima del sprite (Carry Set) |
Si el player esta encima del sprite, ¿debe o no rebotar?
Una ves sabemos que el sprite esta encima, debemos realizar los siguientes chequeos:
- Revisar si el player esta tocando el piso, ya que, si el player esta tocando el piso, no es correcto que rebote.
- Revisar si la velocidad relativa entre el sprite y el player es tal, que el player efectivamente este pisando al sprite.
El primer chequeo es sencillo, podemos realizarlo con las siguientes 2 lineas de codigo:
LDA !PlayerBlockedStatus_S00MUDLR AND #$04
Si luego de usar esas 2 lineas, el valor de A es 0, entonces no esta tocando el piso, en otro casi si esta tocando piso.
El chequeo complicado es el de velocidad relativa, basicamente se divide primero en obtener la velocidad real en Y y luego compararla con la velocidad del player. Para el primer paso utilizaremos la siguiente rutina:
LDA !SpriteBlockedStatus_ASB0UDLR,x AND #$24 BNE ?+ RTL ?+ LDA !SpriteXSpeed,x ROL ROL AND #$01 STA !Scratch45 LDA $15B8|!addr,x CLC ADC #$04 ASL ORA !Scratch45 ASL PHA LDA !SpriteXSpeed,x BPL ?+ EOR #$FF INC A ?+ PLX JSR (?+++,x) RTL ?+++ dw ?++++ ;Right X Speed, Very steep slope left. dw ?+++++ ;Left X Speed, Very steep slope left. dw ?++++++ ;Right X Speed, Steep slope left. dw ?+++++++ ;Left X Speed, Steep slope left. dw ?++++++++ ;Right X Speed, Normal slope left. dw ?+++++++++ ;Left X Speed, Normal slope left. dw ?++++++++++ ;Right X Speed, Gradual slope left. dw ?+++++++++++ ;Left X Speed, Gradual slope left. dw ?++++++++++++ ;No Slope dw ?++++++++++++ ;No Slope dw ?+++++++++++ ;Right X Speed, Gradual slope Right. dw ?++++++++++ ;Left X Speed, Gradual slope Right. dw ?+++++++++ ;Right X Speed, Normal slope Right. dw ?++++++++ ;Left X Speed, Normal slope Right. dw ?+++++++ ;Right X Speed, Steep slope Right. dw ?++++++ ;Left X Speed, Steep slope Right. dw ?+++++ ;Right X Speed, Very steep slope Right. dw ?++++ ;Left X Speed, Very steep slope Right. ?++++ ;Right X Speed, Very steep slope left. LDX !SpriteIndex CLC ASL BPL ?+ LDA #$7F ?+ EOR #$FF INC A STA !SpriteYSpeed,x RTS ?+++++ ;Left X Speed, Very steep slope left. LDX !SpriteIndex CLC ASL BPL ?+ LDA #$7F ?+ STA !SpriteYSpeed,x RTS ?++++++ ;Right X Speed, Steep slope left. LDX !SpriteIndex EOR #$FF INC A STA !SpriteYSpeed,x RTS ?+++++++ ;Left X Speed, Steep slope left. LDX !SpriteIndex STA !SpriteYSpeed,x RTS ?++++++++ ;Right X Speed, Normal slope left. LDX !SpriteIndex LSR EOR #$FF INC A STA !SpriteYSpeed,x RTS ?+++++++++ ;Left X Speed, Normal slope left. LDX !SpriteIndex LSR STA !SpriteYSpeed,x RTS ?++++++++++ ;Right X Speed, Gradual slope left. LDX !SpriteIndex LSR LSR EOR #$FF INC A STA !SpriteYSpeed,x RTS ?+++++++++++ ;Left X Speed, Gradual slope left. LDX !SpriteIndex LSR LSR STA !SpriteYSpeed,x RTS ?++++++++++++ ;No Slope LDX !SpriteIndex STZ !SpriteYSpeed,x RTS
Esta rutina es algo complicada, pero básicamente actualiza el valor de la velocidad Y del sprite dependiendo de si esta en un slope o no. Esta rutina pueden incluirla en la carpeta routines de pixi con el nombre "GetRealSpriteYSpeed".
Luego, para saber si debe o no rebotar, la velocidad del player debe ser mayor (considerando el signo) que la del sprite, de la siguiente manera:
LDA !PlayerYSpeed CMP !SpriteYSpeed,x
Si luego de esas 2 líneas de código, el flag N (BPL salta si es 1 y BMI salta si es 0), es 1, significa que el player debe rebotar. Por lo tanto la rutina completa deberia ser:
;Return Carry Clear if can bounce, Carry set if not CanBounce: LDA !PlayerBlockedStatus_S00MUDLR AND #$04 BNE ?+ %GetRealSpriteYSpeed() LDA !PlayerYSpeed CMP !SpriteYSpeed,x BPL ?+ SEC RTL ?+ CLC RTL
Recomiendo guardar este resultado en alguna scratch que puedas revisar desde los codigos de las cajas de colisión.
¿Cómo se hace rebotar?
Una ves que se cumplen las condiciones debemos hacer que el player rebote, para esto podemos simplemente llamar estas 2 rutinas:
JSL $01AB99|!rom ;Display White Star JSL $01AA33|!rom ;Do the player boost its Y Speed
Sin embargo no basta con esto, también debemos antes de llamar estas rutinas, modificar la posición del player para que este just arriba de la caja, para esto restaremos la posición en el tope de la caja con la posición del la parte de abajo de la caja de colisión del player y le sumaremos el resultado a la posición del player.
GetDeltaPlayerHitboxBottomAndSpriteHitboxTop: LDA $03 STA $45 STZ $46 ;$45 = player hitbox height LDA $09 XBA LDA $01 REP #$20 CLC ADC $45 STA $45 SEP #$20 ;$45 = Player Hitbox Bottom LDA $0B XBA LDA $01 ;A 16 bits = Sprite hitbox top REP #$20 SEC SBC $45 ;A 16 bits = Sprite hitbox top - Player Hitbox Bottom EOR #$FFFF INC A ;A 16 bits = Player Hitbox Bottom - Sprite hitbox top RTS
Esta rutina calculara esa diferencia, ahora debemos usar eso para sumarselo a la posición del player:
JSR GetDeltaPlayerHitboxBottomAndSpriteHitboxTop CLC ADC !PlayerY STA !PlayerY SEP #$20
Esta rutina, calcularia el delta entre ambas posiciones y luego se lo sumaria a la posición del player, ahora debemos además sumarle ese valor a la posición de la caja de colisión, por lo que quedaría asi:
UpdatePlayerPositionAfterBounce: JSR GetDeltaPlayerHitboxBottomAndSpriteHitboxTop PHA CLC ADC !PlayerY STA !PlayerY SEP #$20 LDA $09 XBA LDA $01 REP #$20 STA $45 PLA CLC ADC $45 SEP #$20 STA $01 XBA STA $09 RTS
Cuando una caja de colisión defina que el player debe rebotar, entonces pondremos la variable !PlayerIsAboveSprite,x
en el valor 2. Además actualizaremos la posición del player para que este encima de la caja.