0x06 Ensamblador ZX Spectrum Marciano – Enemigos
En este capítulo de Ensamblador ZX Spectrum Marciano vamos a incluir los enemigos. Lo primero es crear la carpeta Paso06, copiamos desde la carpeta Paso05 los archivos Cargador.tap, Const.asm, Ctrl.asm, Game.asm, Graph.asm, Int.asm, Main.asm, Print.asm, Var.asm y make, o make.bat si estáis trabajando en Windows.
Definimos los enemigos
Los enemigos son elementos móviles, y como tales necesitamos saber la posición actual y la inicial. En total vamos a tener un máximo de veinte enemigos en pantalla, y vamos a usar dos bytes para especificar la posición actual del enemigo y alguna configuración más que nos va a hacer falta.
Abrimos el archivo Var.asm y tras la definición del marco de la pantalla, añadimos la configuración de los enemigos.
; ----------------------------------------------------------------------------- ; Configuración de los enemigos ; ; 2 bytes por enemigo. ; ----------------------------------------------------------------------------- ; Byte 1 | Byte 2 ; ----------------------------------------------------------------------------- ; Bit 0-4: Posición Y | Bit 0-4: Posición X ; Bit 5: Libre | Bit 5: Libre ; Bit 6: Libre | Bit 6: Dirección X 0 = Left 1 = Right ; Bit 7: Activo 1/0 | Bit 7: Dirección Y 0 = Up 1 = Down ; ----------------------------------------------------------------------------- enemiesConfig: db $96, $dd, $96, $d7, $96, $d1, $96, $cb, $96, $c5 db $93, $9d, $93, $97, $93, $91, $93, $8b, $93, $85 db $90, $dd, $90, $d7, $90, $d1, $90, $cb, $90, $c5 db $8d, $9d, $8d, $97, $8d, $91, $8d, $8b, $8d, $85 enemiesConfigIni: db $96, $dd, $96, $d7, $96, $d1, $96, $cb, $96, $c5 db $93, $9d, $93, $97, $93, $91, $93, $8b, $93, $85 db $90, $dd, $90, $d7, $90, $d1, $90, $cb, $90, $c5 db $8d, $9d, $8d, $97, $8d, $91, $8d, $8b, $8d, $85 enemiesConfigEnd:
En el primer byte vamos a tener la coordenada Y, bits del cero al cuatro, y si el enemigo está o no activo, bit siete. En el segundo byte vamos a tener la coordenada X, bits del cero al cuatro, la dirección horizontal, bit seis, y la dirección vertical, bit siete.
Según los bits seis y siete del segundo byte, la dirección del enemigo es:
- 00b $00 Izquierda / Arriba
- 01b $01 Izquierda / Abajo
- 10b $02 Derecha / Arriba
- 11b $03 Derecha / Abajo
Basándonos en esto, vamos a añadir la definición de los gráficos de los enemigos. Justo antes de enemiesConfig añadimos dicha definición.
; ----------------------------------------------------------------------------- ; Gráficos de los enemigos ; ; 00 Up-Left ; 01 Up-Right ; 10 Down-Left ; 11 Down-Right ; ----------------------------------------------------------------------------- enemiesGraph: db $9f, $a0, $a1, $a2
Si buscamos los UDG de los enemigos, veremos que todo concuerda.
Volviendo a la definición de los enemigos, vamos a tener un total de veinte enemigos, repartidos en cuatro filas con cinco enemigos por fila.
Vamos a ver la definición de los enemigos de izquierda a derecha y de arriba a abajo; recordad que trabajamos con las coordenadas invertidas.
$96, $dd | 10010110, 11011101 | Activo Línea 22 Abajo/Derecha Columna 29 |
$96, $d7 | 10010110, 11010111 | Activo Línea 22 Abajo/Derecha Columna 23 |
$96, $d1 | 10010110, 11010001 | Activo Línea 22 Línea/Derecha Columna 17 |
$96, $cb | 10010110, 11001011 | Activo Línea 22 Abajo/Derecha Columna 11 |
$96, $c5 | 10010110, 11000101 | Activo Línea 22 Abajo/Derecha Columna 5 |
$93, $9d | 10010011, 10011101 | Activo Línea 19 Abajo/Izquierda Columna 29 |
$93, $97 | 10010011, 10010111 | Activo Línea 19 Abajo/Izquierda Columna 23 |
$93, $91 | 10010011, 10010001 | Activo Línea 19 Abajo/Izquierda Columna 17 |
$93, $8b | 10010011, 10001011 | Activo Línea 19 Abajo/Izquierda Columna 11 |
$93, $85 | 10010011, 10000101 | Activo Línea 19 Abajo/Izquierda Columna 5 |
$90, $dd | 10010000, 11011101 | Activo Línea 16 Abajo/Derecha Columna 29 |
$90, $d7 | 10010000, 11010111 | Activo Línea 16 Abajo/Derecha Columna 23 |
$90, $d1 | 10010000, 11010001 | Activo Línea 16 Abajo/Derecha Columna 17 |
$90, $cb | 10010000, 11001011 | Activo Línea 16 Abajo/Derecha Columna 11 |
$90, $c5 | 10010000, 11000101 | Activo Línea 16 Abajo/Derecha Columna 5 |
$8d, $9d | 10001101, 10011101 | Activo Línea 13 Abajo/Izquierda Columna 29 |
$8d, $97 | 10001101, 10010111 | Activo Línea 13 Abajo/Izquierda Columna 23 |
$8d, $91 | 10001101, 10010001 | Activo Línea 13 Abajo/Izquierda Columna 17 |
$8d, $8b | 10001101, 10001011 | Activo Línea 13 Abajo/Izquierda Columna 11 |
$8d, $85 | 10001101, 10000101 | Activo Línea 13 Abajo/Izquierda Columna 5 |
Una vez definidos los gráficos de los enemigos y su configuración, podemos proceder a pintarlos.
Pintamos los enemigos
La rutina que pinta los enemigos la vamos a implementar en Print.asm.
PrintEnemies: ld a, $06 call Ink ld hl, enemiesConfig ld d, $14
Cargamos en A la tinta amarilla, LD A, $06, cambiamos la tinta, CALL Ink, cargamos la dirección de memoria de los enemigos en HL, LD HL, enemiesConfig, y el número total de enemigos en D, LD D, $14, veinte enemigos.
printEnemies_loop: bit $07, (hl) jr z, printEnemies_endLoop
Evaluamos si el enemigo está activo, BIT $07, (HL), y de no estarlo saltamos, JR Z, printEnemies_endLoop.
push hl ld a, (hl) and $1f ld b, a
Preservamos el valor de HL, PUSH HL, cargamos el primer byte de la configuración del enemigo en A, LD A, (HL), nos quedamos con la coordenada Y, AND $1F, y la cargamos en B, LD B, A.
inc hl ld a, (hl) and $1f ld c, a call At
Apuntamos HL al segundo byte de la configuración del enemigo, INC HL, cargamos el valor en A, LD A, (HL), nos quedamos con la coordenada X, AND $1F, cargamos el valor en C, LD C, A, y posicionamos el cursor, CALL At.
ld a, (hl) and $c0 rlca rlca ld c, a ld b, $00
Cargamos el segundo byte de la configuración del enemigo en A, LD A, (HL), nos quedamos con los bits de dirección, AND $c0, pasamos el valor a los bits cero y uno, RLCA RLCA, cargamos el valor en C, LD C, A, y ponemos B a cero, LD B, $00.
ld hl, enemiesGraph add hl, bc ld a, (hl) rst $10
Cargamos en HL la dirección de memoria en la que definimos los caracteres para los enemigos, LD HL, enemiesGraph, le sumamos la dirección (izquierda, arriba …) del enemigo, ADD HL, BC, cargamos en A el carácter del enemigo que hay que pintar, LD A, (HL), y lo pintamos, RST $10.
pop hl printEnemies_endLoop: inc hl inc hl dec d jr nz, printEnemies_loop ret
Recuperamos el valor de HL, POP HL, apuntamos HL al primer byte de la configuración del siguiente enemigo, INC HL INC HL, decrementamos D, DEC D, y seguimos hasta que D sea cero y hayamos recorrido todos los enemigos. Finalmente, salimos, RET.
El aspecto final de la rutina es el siguiente:
; ----------------------------------------------------------------------------- ; Pinta los enemigos ; ; Altera el valor de los registros AF, BC, D y HL. ; ----------------------------------------------------------------------------- PrintEnemies: ld a, $06 ; Carga en A la tinta amarilla call Ink ; Cambia la tinta ld hl, enemiesConfig ; Carga la dirección de la configuración ; del enemigo en HL ld d, $14 ; Carga en D 20 enemigos printEnemies_loop: bit $07, (hl) ; Evalúa si el enemigo está activo jr z, printEnemies_endLoop ; Si no lo está, salta push hl ; Preserva el valor de HL ld a, (hl) ; Carga el primer byte de configuración en A and $1f ; Se queda con la coordenda Y ld b, a ; La carga en B inc hl ; Apunta HL al segundo byte ld a, (hl) ; Carga el valor en A and $1f ; Se queda con la coordenada X ld c, a ; La carga en C call At ; Posiciona el cursor ld a, (hl) ; Vuelve a cargar el segudo byte en A and $c0 ; Se queda con la dirección (izquierda ...) rlca ; Pone el valor en los bits 0 y 1 rlca ld c, a ; Carga el valor en C ld b, $00 ; Pone B a cero ld hl, enemiesGraph ; Carga en HL el carácter del gráfico del enemigo add hl, bc ; Le suma la dirección de enemigo (izquierda ...) ld a, (hl) ; Carga en A el gráfico del enemigo rst $10 ; Lo pinta pop hl ; Recupera el valor de HL printEnemies_endLoop: inc hl ; Apunta HL al primer byte de la configuración inc hl ; del enemigo siguientes dec d ; Decrementa D jr nz, printEnemies_loop ; hasta que D sea 0 ret
Para probar si funciona, vamos a ir a Main.asm y justo antes de Main_loop vamos a añadir las líneas siguientes:
ld a, $01 call LoadUdgsEnemies call PrintEnemies
Cargamos el nivel uno en A, LD A, $01, cargamos los gráficos de los enemigos del nivel uno en udgsExtension, CALL LoadUdgsEnemies, y pintamos los enemigos, CALL PrintEnemies.
Compilamos, cargamos en el emulador y vemos los resultados.

Movemos los enemigos
Los enemigos no los vamos a mover en cada iteración del bucle, como hacemos con el disparo, los vamos a mover cada N interrupciones, por lo qué lo primero que vamos a hacer es añadir otro comentario en la etiqueta flag, al inicio de Main.asm.
; Bit 2 -> se deben mover los enemigos 0 = No, 1 = Sí
Lo siguiente es establecer los límites de la pantalla hasta dónde los enemigos pueden llegar; estos límites los vamos a establecer en Const.asm.
; Topes de los enemigos ENEMY_TOP_T: EQU COR_Y - MIN_Y ENEMY_TOP_B: EQU COR_Y - MAX_Y + $01 ENEMY_TOP_L: EQU COR_X - MIN_X ENEMY_TOP_R: EQU COR_X - MAX_X
Los límites que hemos establecido son arriba, abajo, izquierda y derecha.
Para que los enemigos se muevan cada N interrupciones, y como en flags vamos a usar un bit para indicar si se deben mover o no, vamos a ir al archivo Int.asm y vamos a activar dicho bit, para lo que añadimos la siguiente línea justo debajo de SET $00, (HL):
set $02, (hl)
Ya tenemos todos listo para poder implementar en Game.asm la rutina que mueve los enemigos.
MoveEnemies: ld hl, flags bit $02, (hl) ret z res $02, (hl)
Cargamos en HL la dirección de memoria de flags, LD HL, flags, comprobamos si el bit de movimiento de los enemigos está activo, BIT $02, (HL), y si no está activo, salimos, RET Z. En el caso de estar activo, lo desactivamos para que no pase por aquí en la próxima iteración de Main_loop, RES $02, (HL).
ld d, $14 ld hl, enemiesConfig moveEnemies_loop: bit $07, (hl) jr z, moveEnemies_endLoop
Cargamos en D el número total de enemigos (veinte), LD D, $14, cargamos en HL la dirección de memoria de la configuración de los enemigos, LD HL, enemiesConfig, comprobamos si el enemigo está activo, BIT $07, (HL), y si no está activo saltamos al final del bucle, JR Z, moveEnemies_endLoop.
push hl ld a, (hl) and $1f ld b, a inc hl ld a, (hl) and $1f ld c, a call DeleteChar pop hl
Preservamos el valor de HL, PUSH HL, cargamos en A el primer byte de la configuración del enemigo, LD A, (HL), nos quedamos con la coordenada Y, AND $1F, y la cargamos en B, LD B, A.
Apuntamos HL al segundo byte de la configuración del enemigo, INC HL, cargamos el valor en A, LD A, (HL), nos quedamos con la coordenada X, AND $1F, y la cargamos en C, LD C, A.
Borramos el enemigo, CALL DeleteChar, y recuperamos el valor de HL, POP HL.
ld b, (hl) inc hl ld c, (hl)
Cargamos el primer byte de la configuración del enemigo en B, LD B, (HL), apuntamos HL al segundo byte de la configuración, INC HL, y cargamos el valor en C, LD C, (HL).
moveEnemies_X: ld a, c and $1f bit $06, c jr nz, moveEnemies_X_right
Cargamos el valor del segundo byte de la configuración del enemigo en A, LD A, C, y nos quedamos con la coordenada X, AND $1F.
Comprobamos el bit de dirección horizontal del enemigo, BIT $06, C, si está a uno, el enemigo se desplaza hacia la derecha y salta, JR Z, moveEnemies_X_right. Si no ha saltado, el enemigo se mueve hacia la izquierda.
moveEnemies_X_left: inc a sub ENEMY_TOP_L jr z, moveEnemies_X_leftChg inc c jr moveEnemies_Y moveEnemies_X_leftChg: set $06, c jr moveEnemies_Y
Incrementamos A para que apunte a la columna a la izquierda de la actual, INC A, restamos el tope por la izquierda, SUB ENEMY_TOP_L, y si el resultado es cero, ha llegado al tope y salta para cambiar la dirección, JR Z, moveEnemies_X_leftChg.
Si no hay que cambiar la dirección, apunta C a la columna a la izquierda de la actual, INC C, y salta al movimiento vertical, JR moveEnemies_Y.
Si hay que cambiar la dirección, activa el bit seis de C para poner dirección horizontal hacia la derecha, SET $06, C, y salta al movimiento vertical, JR moveEnemies_Y.
Si el enemigo no se mueve hacia la izquierda, se mueve hacia la derecha.
moveEnemies_X_right: dec a sub ENEMY_TOP_R jr z, moveEnemies_X_rightChg dec c jr moveEnemies_Y moveEnemies_X_rightChg: res $06, c
Decrementamos A para que apunte a la columna a la derecha de la actual, DEC A, restamos el tope por la derecha, SUB ENEMY_TOP_R, y si el resultado es cero, ha llegado al tope y salta para cambiar la dirección, JR Z, moveEnemies_X_rightChg.
Si no hay que cambiar la dirección, apunta C a la columna a la derecha de la actual, DEC C, y salta al movimiento vertical, JR moveEnemies_Y.
Si hay que cambiar la dirección, desactiva el bit seis de C para poner dirección horizontal hacia la izquierda, RES $06, C, y empezamos con el movimiento vertical.
moveEnemies_Y: ld a, b and $1f bit $07, c jr nz, moveEnemies_Y_down
Cargamos en A el valor del primer byte de la configuración del enemigo, LD A, B, y nos quedamos con la coordenada Y, AND $1F.
Comprobamos el bit siete de C para saber la dirección vertical del enemigo, BIT $07, C, y si está a uno el enemigo se mueve hacia abajo y salta, JR NZ, moveEnemies_Y_down.
Si el bit está a cero, el enemigo se mueve hacia arriba.
moveEnemies_Y_up: inc a sub ENEMY_TOP_T jr z, moveEnemies_Y_upChg inc b jr moveEnemies_endMove moveEnemies_Y_upChg: set $07, c jr moveEnemies_endMove
Incrementamos A para que apunte a la línea superior a la actual, INC A, restamos el tope por arriba, SUB ENEMY_TOP_T, y si el resultado es cero, hemos llegado al tope y saltamos para cambiar la dirección, JR Z, moveEnemies_Y_upChg.
Si no hemos llegado al tope, incrementamos B para que apunte a la línea superior a la actual, INC B, y saltamos al final del bucle, JR moveEnemies_endMove.
Si hay que cambiar la dirección, activamos el bit siete de C para cambiar la dirección hacia abajo, SET $07, C, y saltamos al final del bucle, JR moveEnemies_endMove.
Si el enemigo no se mueve hacia arriba, se mueve hacia abajo.
moveEnemies_Y_down: dec a sub ENEMY_TOP_B jr z, moveEnemies_Y_downChg dec b jr moveEnemies_endMove moveEnemies_Y_downChg: res $07, c
Decrementamos A para que apunte a la línea inferior a la actual, DEC A, restamos el tope por abajo, SUB ENEMY_TOP_B, y si el resultado es cero, hemos llegado al tope y saltamos para cambiar la dirección, JR Z, moveEnemies_Y_downChg.
Si no hemos llegado al tope, decrementamos B para que apunte a la línea inferior a la actual, DEC B, y saltamos al final del bucle, JR moveEnemies_endMove.
Si hay que cambiar la dirección, desactivamos el bit siete de C para cambiar la dirección hacia arriba, RES $07, C.
moveEnemies_endMove: ld (hl), c dec hl ld (hl), b moveEnemies_endLoop: inc hl inc hl dec d jr nz, moveEnemies_loop
Actualizamos en memoria el segundo byte de la configuración del enemigo, LD (HL), C, apuntamos HL al primer byte, DEC HL, y actualizamos en memoria, LD (HL), B.
Apuntamos HL al primer byte de la configuración del siguiente enemigo, INC HL, INC HL, decrementamos D, DEC D, y seguimos en el bucle hasta que D sea cero y hayamos recorrido los veinte enemigos, JR Z, moveEnemies_loop.
moveEnemies_end: call PrintEnemies ret
Pintamos los enemigos en las nuevas posiciones, CALL PrintEnemies, y salimos, RET.
Con esto, hemos implementado la rutina que mueve los enemigos, cuyo aspecto final es el siguiente:
; ----------------------------------------------------------------------------- ; Mueve los enemigos. ; ; Altera el valor de lo registros AF, BC, D y HL. ; ----------------------------------------------------------------------------- MoveEnemies: ld hl, flags ; Cargamos la dirección de memoria de flags en HL bit $02, (hl) ; Comprueba si el bit 2 está activo ret z ; Si no es así, sale res $02, (hl) ; Desactiva el bit 2 de flags ld d, $14 ; Carga en D el número total de enemigos (20) ld hl, enemiesConfig ; Cara en HL la dirección de la configuración ; de los enemigos moveEnemies_loop: bit $07, (hl) ; Comprueba si el enemigo está activo jr z, moveEnemies_endLoop ; Si no lo está, salta a final del bucle push hl ; Preserva el valor de HL ld a, (hl) ; Carga en A el primer byte de la cofiguración and $1f ; Se queda con la coordenada Y ld b, a ; Carga el valor en B inc hl ; Apunta HL al segundo byte de la configuración ld a, (hl) ; Carga el valor en A and $1f ; Se queda con la coordeanda X ld c, a ; Carga el valor en C call DeleteChar ; Borra el enemigo pop hl ; Recupera el valor de HL ld b, (hl) ; Carga en B el primer byte de la configuración inc hl ; Apunta HL al segundo byte de la configuración ld c, (hl) ; Carga en C el segundo byte de la configuración moveEnemies_X: ld a, c ; Carga en A el segundo byte de la configuración and $1f ; Se queda con la coordeada X bit $06, c ; Evalúa la dirección horizontal del enemigo jr nz, moveEnemies_X_right ; Si está a uno, hacia la derecha, salta moveEnemies_X_left: inc a ; Apunta A a la columna anterior sub ENEMY_TOP_L ; Resta el tope por la izquierda jr z, moveEnemies_X_leftChg ; Si es cero, ha llegado al tope, salta inc c ; Apunta C a la columna anterior jr moveEnemies_Y ; Salta al movimiento vertical moveEnemies_X_leftChg: set $06, c ; Pone la dirección horizontal hacia la derecha jr moveEnemies_Y ; Salta al movimiento vertical moveEnemies_X_right: dec a ; Apunta A a la columna posterior sub ENEMY_TOP_R ; Resta el tope por la derecha jr z, moveEnemies_X_rightChg ; Si es cero, ha llegado al tope, salta dec c ; Apunta C a la columna posterior jr moveEnemies_Y ; Salta al movimiento vertical moveEnemies_X_rightChg: res $06, c ; Pone la dirección horizontal hacia la izquierda moveEnemies_Y: ld a, b ; Carga en A el primer byte de la configuración and $1f ; Se qued con la coordenda Y bit $07, c ; Evalúa la dirección vertical del enemigo jr nz, moveEnemies_Y_down ; Si está a uno, hacia abajo, salta moveEnemies_Y_up: inc a ; Apunta A a la línea anterior sub ENEMY_TOP_T ; Resta el tope por arriba jr z, moveEnemies_Y_upChg ; Si es cero, ha llegado al tope, salta inc b ; Apunta B a la línea posterior jr moveEnemies_endMove ; Salta al final del bucle moveEnemies_Y_upChg: set $07, c ; Pone la dirección vertical hacia abajo jr moveEnemies_endMove ; Salta al final del bucle moveEnemies_Y_down: dec a ; Apunta A a la línea posterior sub ENEMY_TOP_B ; Resta el tope por abajo jr z, moveEnemies_Y_downChg ; Si es cero, ha llegado al tope, salta dec b ; Apunta B a la línea posterior jr moveEnemies_endMove ; Salta al final del bucle moveEnemies_Y_downChg: res $07, c ; Pone la dirección vertical hacia arriba moveEnemies_endMove: ld (hl), c ; Actualiza el segundo byte de la configuración dec hl ; Apunta HL al primer byte de la configuración ld (hl), b ; Actualiza el primer byte de la configuración moveEnemies_endLoop inc hl inc hl ; Aputa HL al primer byte de la configuración ; del siguiente enemigo dec d ; Decrementa D jr nz, moveEnemies_loop ; Hasta que D sea cero (20 enemigos) moveEnemies_end: call PrintEnemies ; Pinta los enemigos ret
Ha llegado el momento de ver como se mueven los enemigos. Abrimos el archivo Main.asm y en la etiqueta Main_loop, justo debajo de CALL MoveShip, añadimos la línea siguiente:
call MoveEnemies
Compilamos, cargamos en el emulador y vemos los resultados.

¿Qué tal? ¿Se mueven los enemigos? Sí, se mueven, pero van demasiado deprisa y el disparo se ha vuelto más lento. Vamos a ralentizar el movimiento de los enemigos, y lo vamos hacer desde la rutina de la interrupciones, así que vamos al archivo Int.asm.
Vamos a hacer algo parecido a lo que hicimos en PorompomPong, vamos a añadir un contador al final del archivo para controlar cuando activamos el movimiento de los enemigos.
countEnemy: db $00
Ahora entre las líneas SET $00, (HL) y SET $02, (HL) vamos a implementar el uso de este contador.
ld a, (countEnemy) inc a ld (countEnemy), a sub $03 jr nz, Isr_end ld (countEnemy), a
Cargamos en A el valor del contador, LD A, (countEnemy), incrementamos A, INC A, y actualizamos el contador en memoria, LD (countEnemy), A. Restamos a A el valor que tiene que alcanzar el contador para activar el movimiento, SUB $03, y si no ha llegado salta al final de la rutina, JR NZ, Isr_end.
Si ya ha alcanzado el valor, pone a cero el contador, LD (countEnemy), A, y activa el bit para mover los enemigos, SET $02, (HL).
Compilamos, cargamos en el emulador y vemos que hemos recuperado la velocidad del disparo y que los enemigos se siguen moviendo rápido.
Ensamblador ZX Spectrum, conclusión
Ya tenemos en movimiento todos los elementos de nuestro juego.
En el siguiente capítulo implementaremos la detección de colisiones, para poder matar a los enemigos y que ellos nos maten a nosotros, y poder ir cambiando de nivel, hasta un total de treinta.
Podéis descargar todo el código generado desde aquí.
Enlaces de interés
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Ensamblador ZX Spectrum Pong
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF y EPUB
- Proyecto en itch.io
Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.
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.