0x09 Ensamblador ZX Spectrum Pong – Cambio de dirección/velocidad de la bola al golpear la pala
En esta nueva entrega de Ensamblador ZX Spectrum Pong, vamos a prescindir de parte de lo que hemos implementado en la entrega anterior. La velocidad de la bola va a cambiar dependiendo de con qué parte de la pala colisione.
Tabla de contenidos
- Ensamblador ZX Spectrum – Cambio de dirección/velocidad de la bola al golpear la pala
- Cambio de velocidad, inclinación y dirección
- Enlaces de interés
- Vídeo
Ensamblador ZX Spectrum – Cambio de dirección/velocidad de la bola al golpear la pala
Creamos la carpeta Paso09 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso08.
Lo primero que vamos a hacer es quitar la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3.
Abrimos el archivo Controls.asm y en la rutina ScanKeys, borramos todas las líneas hasta la etiqueta scanKeys_ctrl, quedando el inicio de la rutina de la siguiente manera.
ScanKeys:
ld d, $00
scanKeys_A:
Si compilamos y cargamos en el emulador, vemos que la velocidad de la bola no cambia.
Vamos a añadir nuevas constantes y variables en el archivo Sprite.asm, para poder controlar la inclinación de la bola. También vamos a cambiar los sprites de las palas; ambas van a dibujar cuatro píxeles, pero en ambos casos dibujaremos los más cercanos al centro de la pantalla.
Añadimos las constantes que indican la rotación a asignar a la bola cuando se produce la colisión con la pala.
CROSS_LEFT_ROT: EQU $ff
CROSS_RIGHT_ROT: EQU $01
Añadimos la posición inicial de la bola, y el número acumulado de movimientos que debe llevar la bola para cambiar la posición Y. Este último dato lo vamos a usar para cambiar la inclinación de la bola.
BALLPOS_INI: EQU $4850
ballMovCount: db $00
Cambiamos la configuración inicial de la bola y la documentación (comentarios) de la misma.
; Velocidad y dirección de la bola.
; bits 0 a 3: Movimientos de la bola para que cambie la posición Y.
; Valores f = semiplano, 2 = semi diagonal, 1 = diagonal
; bits 4 y 5: Velocidad de la bola: 1 muy rápido, 2 (rápido) y 3 (lento)
; bit 6: Dirección X: 0 derecha / 1 izquierda
; bit 7: Dirección Y: 0 arriba / 1 abajo
ballSetting:
db $31 ; 0011 0001
Según la nueva configuración, la bola inicialmente se mueve hacia la derecha y hacia arriba, con una velocidad lenta, y en cada movimiento cambia la posición Y (va en diagonal).
Añadimos distintos sprites para las palas y eliminamos el anterior.
PADDLE: EQU $3c ; ¡ELIMINAR!
PADDLE1: EQU $0f ; ¡NUEVO!
PADDLE2: EQU $f0 ; ¡NUEVO!
Por último, añadimos las posiciones iniciales de las palas.
PADDLE1POS_INI: EQU $4861
PADDLE2POS_INI: EQU $487e
Hemos añadido sprites distintos para cada pala y eliminado la constante que usábamos para pintar las palas; si compilamos, nos dará errores. Vamos a solucionar esos errores modificando la rutina PrintPaddle de Video.asm.
La rutina PrintPaddle recibe en el registro HL la posición de la pala. En el registro C recibirá el sprite de la pala.
Modificamos la línea justo debajo de la etiqueta printPaddle_loop.
ld (hl), PADDLE
Y la dejamos como sigue.
ld (hl), c
Compilamos, y aunque no da ningún error, al cargar en el emulador vemos que los resultados no son los deseados.
La pala que pinta no se corresponde con el sprite que hemos definido, esto es debido a que no hemos cargado en C el sprite que debe pintar.
Abrimos el archivo Main.asm, y buscamos la etiqueta loop_continue. A partir de la línea 5 es donde imprimimos las palas, cargando el HL la posición de la pala y llamando al pintado de la misma. Antes de llamar al pintado de la pala, debemos especificar qué sprite debe pintar.
Este es el aspecto una vez hecha la modificación.
ld hl, (paddle1pos)
ld c, PADDLE1 ; ¡NUEVO!
call PrintPaddle
ld hl, (paddle2pos)
ld c, PADDLE2 ; ¡NUEVO!
call PrintPaddle
Compilamos, abrimos en el emulador, y comprobamos que las palas se vuelven a pintar bien.
Aprovechando que estamos en Main.asm, vamos a cambiar un comportamiento del que quizá no os habéis percatado. Cuando se acaba un partido, y al iniciar otro, las palas siguen en la misma posición donde estaban al acabar el partido anterior, y la bola sale desde el campo del jugador que anotó el último punto.
Para modificar este comportamiento, vamos a añadir las siguientes líneas antes de la etiqueta Loop.
ld hl, BALLPOS_INI
ld (ballPos), hl
ld hl, PADDLE1POS_INI
ld (paddle1pos), hl
ld hl, PADDLE2POS_INI
ld (paddle2pos), hl
Con estas líneas situamos la bola y las palas en sus posiciones iniciales.
Si compilamos, vemos que nos da un error.
ERROR on line 68 of file Main.asm
ERROR: Relative jump out of range
Este error es debido a que, al ir añadiendo líneas, tenemos algún JR que está fuera de rango. JR solo puede saltar 127 bytes hacia adelante o 128 hacia atrás, y tenemos algún JR que salta a alguna dirección fuera de este rango. En concreto, tenemos al final del archivo Main.asm, dos JR Main y un JR Loop. Sustituimos estos tres JR por JP, y solucionamos el error. JP ocupa un byte más que JR, por lo que nuestro programa acaba de crecer 3 bytes, pero hemos reducido 6 ciclos de reloj.
Compilamos, cargamos en el emulador y comprobamos que, al acabar la partida e iniciar otra, tanto la bola como las palas vuelven a su posición inicial.
Cambio de velocidad, inclinación y dirección
Vamos a implementar el cambio de velocidad, inclinación y dirección de la bola al colisionar con las palas.
Abrimos el archivo Game.asm y buscamos la etiqueta checkBallCross_left. Tres líneas por encima encontramos.
ld a, $ff
Modificamos esta línea y la dejamos como sigue.
ld a, CROSS_LEFT_ROT
Buscamos la etiqueta CheckCrossX. Tres líneas por encima encontramos.
ld a, $01
Modificamos esta línea y la dejamos como sigue.
ld a, CROSS_RIGHT_ROT
Hemos cambiado los valores por constantes, para si en un futuro hay que cambiar los valores tenerlos mejor localizados.
El siguiente paso es cambiar la configuración de la bola, dependiendo de en qué parte de la pala colisiona. Vamos a dividir la pala en 5 partes, dependiendo de dónde colisione la bola el comportamiento será como sigue.
Zona de golpeo | Dirección vertical | Inclinación | Velocidad |
---|---|---|---|
1/5 | Arriba | Diagonal | 3 lento |
2/5 | Arriba | Semi diagonal | 2 normal |
3/5 | No cambia | Semi plano | 1 rápido |
4/5 | Abajo | Semi diagonal | 2 normal |
5/5 | Abajo | Diagonal | 3 lento |
Localizamos la etiqueta CheckCrossY, nos vamos a la penúltima línea, XOR A, e implementamos justo antes de ella.
ld a, c
sub $15
ld c, a
ld a, b
add a, $04
ld b, a
Cuando llegamos a este punto, en C tenemos la posición del penúltimo scanline de la pala, y en B la posición de la bola. Ambas posiciones están en formato TTLLLSSS.
Cargamos en A la posición del penúltimo scanline de la pala, LD A, C, nos posicionamos en el primero, SUB $15, y volvemos a cargar el valor en C, LD C, A. Cargamos en A la posición de la bola, LD A, B, nos posicionamos en la parte baja de la bola, ADD A, $04, y volvemos a cargar el valor en B, LD B, A.
A partir de aquí implementamos el cambio de comportamiento, dependiendo del lugar de colisión de la bola.
checkCrossY_1_5:
ld a, c
add a, $04
cp b
jr c, checkCrossY_2_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la primera parte, ADD A, $04, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_2_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.
ld a, (ballSetting)
and $40
or $31
jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 3 e inclinación diagonal, OR $31. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la primera parte de la pala, comprobamos si lo ha hecho con la segunda.
checkCrossY_2_5:
ld a, c
add a, $09
cp b
jr c, checkCrossY_3_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la segunda parte, ADD A, $09, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_3_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.
ld a, (ballSetting)
and $40
or $22
jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 2 e inclinación semi diagonal, OR $22. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la segunda parte de la pala, comprobamos si lo ha hecho con la tercera.
checkCrossY_3_5:
ld a, c
add a, $0d
cp b
jr c, checkCrossY_4_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la tercera parte, ADD A, $0D, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_4_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.
ld a, (ballSetting)
and $c0
or $1f
jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal y con la vertical (ya vienen calculadas), AND $C0, y ponemos velocidad 1 e inclinación semi plana, OR $1F. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la tercera parte de la pala, comprobamos si lo ha hecho con la cuarta.
checkCrossY_4_5:
ld a, c
add a, $11
cp b
jr c, checkCrossY_5_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la cuarta parte, ADD A, $11, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_5_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración.
ld a, (ballSetting)
and $40
or $a2
jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 2 e inclinación semi diagonal, OR $A2. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la cuarta parte de la pala, lo ha hecho con la quinta.
checkCrossY_5_5:
ld a, (ballSetting)
and $40
or $b1
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 3 e inclinación diagonal, OR $B1.
Por último, justo por encima de XOR A, vamos a añadir la etiqueta de fin de función a la que hemos estado haciendo referencia, y vamos a cargar la nueva configuración de la bola en memoria.
checkCrossY_end:
ld (ballSetting), a
Después de XOR A, vamos a poner el contador de movimientos de la bola a 0.
ld (ballMovCount), a
El aspecto final de la rutina es el siguiente.
; -----------------------------------------------------------------------------
; Evalúa si la bola colisiona en el eje Y con la pala.
; En el caso de colisionar, actualiza la configuración de la bola.
; Entrada: HL = Posición de la pala
; Salida: Z = Colisiona.
; NZ = No colisiona.
; Altera el valor de los registros AF, BC y HL.
; -----------------------------------------------------------------------------
CheckCrossY:
call GetPtrY ; Obtiene la posición vertical de la pala (TTLLLSSS)
; La posición devuelta apunta al primer scanline de la pala que está a 0
; apunta al siguiente
inc a
ld c, a ; Carga el valor en C
ld hl, (ballPos) ; Carga en HL la posición de la bola
call GetPtrY ; Obtiene la posición vertical de la bola (TTLLLSSS)
ld b, a ; Carga el valor en B
; Comprueba si la bola pasa por encima de la pala
; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0
; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º
add a, $04 ; Apunta la posición de la bola al 5º scanline
sub c ; Resta a la posición de la bola, la posición de la pala
ret c ; Si hay acarreo sale porque la bola pasa por encima
; Comprueba si la bola pasa por debajo de la pala
ld a, c ; Carga la posición vertical de la pala en A
add a, $16 ; Le suma 22 para apuntar al penúltimo scanline,
; último que no es 0
ld c, a ; Lo vuelve a cargar en C
ld a, b ; Carga la posición vertical de la bola
inc a ; Le suma 1 para apuntar el scanline 1, primero que no es 0
sub c ; Resta a la posición de la bola, la posición de la pala
ret nc ; Si no hay acarreo la bola pasa por debajo
; de la pala o colisiona en el último scanline.
; En este último caso se activa el flag Z
; Dependiendo de donde sea la colisión, se asigna grado de inclinación
; y velocidad a la bola
ld a, c ; Carga la posición del penúltimo scanline de la pala en A
sub $15 ; Lo vuelve a posicionar en el primero
ld c, a ; Carga el valor en C
ld a, b ; Carga en A la posición de la bola
add a, $04 ; Se posiciona en la parte baja de la bola
ld b, a ; Carga el valor en B
checkCrossY_1_5:
ld a, c ; Carga la posición vertical de la pala en A
add a, $04 ; Se posiciona en el último scanline de 1/5
cp b ; Lo compara con la posición de la bola
jr c, checkCrossY_2_5 ; La bola está más abajo, salta
ld a, (ballSetting) ; Carga la configuración de la bola en A
and $40 ; Se queda con la dirección horizontal
or $31 ; Hacia arriba, velocidad 3 e inclinación diagonal
jr checkCrossY_end ; Fin de la rutina
checkCrossY_2_5:
ld a, c ; Carga la posición vertical de la pala en A
add a, $09 ; Se posiciona en el último byte de 2/5
cp b ; Lo compara con la posición de la bola
jr c, checkCrossY_3_5 ; La bola está más abajo, salta
ld a, (ballSetting) ; Carga la configuración de la bola en A
and $40 ; Se queda con la dirección horizontal
or $22 ; Hacia arriba, velocidad 2 e inclinación semi diagonal
jr checkCrossY_end ; Fin de la rutina
checkCrossY_3_5:
ld a, c ; Carga la posición vertical de la pala en A
add a, $0d ; Se posiciona en el último byte de 3/5
cp b ; Lo compara con la posición de la bola
jr c, checkCrossY_4_5 ; La bola está más abajo, salta
ld a, (ballSetting) ; Carga la configuración de la bola en A
and $c0 ; Se queda con la dirección horizontal y vertical
or $1f ; Hacia arriba/abajo, velocidad 1 e inclinación semi plano
jr checkCrossY_end ; Fin de la rutina
checkCrossY_4_5:
ld a, c ; Carga la posición vertical de la pala en A
add a, $11 ; Se posiciona en el último byte de 4/5
cp b ; Lo compara con la posición de la bola
jr c, checkCrossY_5_5 ; La bola está más abajo, salta
ld a, (ballSetting) ; Carga la configuración de la bola en A
and $40 ; Se queda con la dirección horizontal y vertical
or $a2 ; Hacia abajo, velocidad 2 e inclinación semi diagonal
jr checkCrossY_end ; Fin de la rutina
checkCrossY_5_5:
ld a, (ballSetting) ; Carga la configuración de la bola en A
and $40 ; Se queda con la dirección horizontal
or $b1 ; Hacia abajo, velocidad 3 e inclinación diagonal
; Hay colisión
checkCrossY_end:
ld (ballSetting), a ; Carga en memoria la configuración actual de la bola
xor a ; Activa el flag Z y pone A = 0
ld (ballMovCount), a ; Pone el contador de movimientos de la bola a 0
ret
Compilamos, cargamos en el emulador y vemos los resultados.
Vemos que la velocidad sí cambia dependiendo de dónde colisiona la bola, pero no la inclinación. Además, al marcar un tanto, la velocidad no se reinicia, lo cual hace que sea muy difícil seguir jugando si la bola va a la velocidad máxima.
¿Por qué cambia la velocidad, pero no la inclinación?
Si hacemos memoria, en el paso anterior implementamos la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3. De hecho, este paso lo iniciamos avisando de que íbamos a prescindir de esta implementación, pero de lo que no se ha prescindido es del cambio que hicimos en Main.asm para tener en cuenta la velocidad de la bola que marque la configuración; por eso la velocidad cambia.
Nos falta la implementación para tener en cuenta la inclinación, y para que cuando se marca un punto, velocidad e inclinación de la bola se reinicien.
Vamos a empezar con el cambio de inclinación. Seguimos en el archivo Game.asm, implementando la rutina que va a cambiar la posición Y de la bola. La vamos a implementar después del RET de la etiqueta moveBall_end.
MoveBallY:
ld a, (ballSetting)
and $0f
ld d, a
Cargamos en A la configuración de la bola, LD A, (ballSetting), nos quedamos con la inclinación, AND $0F, y cargamos el valor en D, LD A, D.
ld a, (ballMovCount)
inc a
ld (ballMovCount), a
cp d
ret nz
Cargamos los movimientos de la bola en A, LD A, (ballMovCount), lo incrementamos en 1, INC A, cargamos el valor en memoria, LD (ballMovCount), A, y lo comparamos con D, que contiene el número de movimientos necesarios para cambiar la posición Y de la bola, CP D. Si no son iguales, no se ha llegado al valor necesario y salimos, RET NZ.
xor a
ld (ballMovCount), a
ret
Si hemos llegado al valor, ponemos A = 0 y activamos el flag Z, XOR A, ponemos a 0 los movimientos acumulados de la bola, LD (ballMovCount), A, y salimos, RET. Al activar el flag Z se indica, a quien llame, que se debe cambiar la posición Y de la bola.
El aspecto final de la rutina es el siguiente.
; -----------------------------------------------------------------------------
; Cambia la posición Y de la bola
; Altera el valor de los registros AF y D.
; -----------------------------------------------------------------------------
MoveBallY:
ld a, (ballSetting) ; Carga en A la configuración de la bola
and $0f ; Se queda con la inclinación
ld d, a ; Carga el valor en D
ld a, (ballMovCount) ; Carga en A los movimientos acumulados de la bola
inc a ; Incrementa A
ld (ballMovCount), a ; Carga el valor en memoria
cp d ; Lo compara con la inclinación
ret nz ; Si no son iguales, sale. No se cambia la posición
; La posición debe cambiar
xor a ; Pone A = 0 y activa el flag Z
ld (ballMovCount), a ; Pone los movimientos acumulados de la bola a 0
ret
Localizamos la etiqueta moveBall_up, y entre las líneas JR Z, moveBall_upChg y CALL PreviousScan, añadimos las siguientes líneas.
call MoveBallY
jr nz, moveBall_x
Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.
Localizamos la etiqueta moveBall_down, y entre las líneas JR Z, moveBall_downChg y CALL NextScan, añadimos las siguientes líneas.
call MoveBallY
jr nz, moveBall_x
Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.
Compilamos, cargamos en el emulador, y comprobamos que ahora cambian la inclinación y la velocidad.
Por último, vamos a hacer que cuando se marque un punto, se reinicien la velocidad y la inclinación de la bola.
Localizamos la rutina SetBallLeft, eliminamos la línea AND $BF, y la sustituimos por las siguientes.
and $80
or $31
Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la derecha, velocidad 3 e inclinación diagonal, OR $31.
Antes de la instrucción RET, añadimos las siguientes líneas.
ld a, $00
ld (ballMovCount), a
Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.
Localizamos la rutina SetBallRight y eliminamos la línea OR $40 y la sustituimos por las siguientes.
and $80
or $71
Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la izquierda, velocidad 3 e inclinación diagonal, OR $11.
Antes de la instrucción RET, añadimos las siguientes líneas.
ld a, $00
ld (ballMovCount), a
Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.
El aspecto final de ambas rutinas es el siguiente.
; -----------------------------------------------------------------------------
; Posiciona la bola a la izquierda.
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
SetBallLeft:
ld hl, $4d60 ; Carga en HL la posición de la bola
ld (ballPos), hl ; Carga el valor en memoria
ld a, $01 ; Carga 1 en A
ld (ballRotation), a ; Lo carga en memoria Rotación = 1
ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola
and $80 ; Se queda con la dirección Y
or $31 ; Pone dirección X a derecha, velocidad 3
; e inclinación diagonal
ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria
ld a, $00
ld (ballMovCount), a
ret
; -----------------------------------------------------------------------------
; Posiciona la bola a la derecha.
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
SetBallRight:
ld hl, $4d7e ; Carga en HL la posición de la bola
ld (ballPos), hl ; Carga el valor en memoria
ld a, $ff ; Carga -1 en A
ld (ballRotation), a ; Lo carga en memoria Rotación = -1
ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola
and $80 ; Se queda con la dirección Y
or $71 ; Pone dirección X a izquierda, velocidad 3
; e inclinación diagonal
ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria
ld a, $00
ld (ballMovCount), a
ret
Compilamos, cargamos en el emulador y vemos los resultados, que deben ser los esperados, aunque la bola va algo lenta, ¿o no?
¿Os habéis fijado que cuando la bola golpea en la parte más baja de la pala no cambia ni dirección vertical, ni inclinación, ni velocidad? ¿Sabéis a qué se debe? Al final del tutorial veremos el por qué.
Podéis descargar todo el código que hemos generado.
Enlaces de interés
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF
- Poyecto en itch.io.
- Archivo .dsk con los juegos de los tutoriales
- Personalización y depuración con ZEsarUX
Vídeo
Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.
Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.
No olvides visitar las webs amigas.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.