6. Implementiamo il primo Nemico
2024-04-10
//Riccardo Sacchetto
//4 min read
Finalmente è giunta l'ora di alzare un po' l'asticella della difficoltà inserendo nel nostro platformer il primo nemico.
Come primo esempio giocattolo proveremo a implementare un semplicissimo mostro che, senza fare salti o evitare ostacoli, si muoverà avanti e indietro in un'area ben individuata.
Utilizzando l'approccio OOP di Python sarà facilissimo implementare una volta per tutte la logica utile ad animare questo NPC per poi utilizzarla tutte le volte che si rivelerà necessario. Fai solo attenzione a non rendere le tue mappe troppo affollate!
Modifichiamo il nostro gioco
Parti dal codice del capitolo precedente e inserisci queste modifiche. Se fatichi a orientarti, in fondo alla pagina trovi l'esempio completo su cui puoi basarti per capire dove mettere le mani.
Iniziamo:
Spiegazione passo per passo
-
Prima di tutto, creiamo una nuova classe
SimpleEnemydopoAnimatedPlayerSprite:class SimpleEnemy(Sprite):- Come nel capitolo 4, iniziamo la nostra avventura definendo una nuova classe che estende
Sprite; dopotutto, anche i nemici sono personaggi del gioco!
- Come nel capitolo 4, iniziamo la nostra avventura definendo una nuova classe che estende
-
Aggiungiamo ora l'init di questa classe:
def __init__(self, enemy_image, min_x = 0, max_x = 0, center_y = 0, speed = 5): super().__init__(enemy_image) self.speed = speed self.min_x = min_x self.max_x = max_x self.center_x = (min_x + max_x)/2 self.center_y = center_y- Come parametri riceviamo ben 5 variabili,
enemy_image,min_x,max_x,center_yespeed:- La prima, che conterrà il percorso al PNG con la texture del nostro nemico, verrà trasferita immediatamente al costruttore della superclasse (
Sprite, nel nostro caso) per utilizzarla - Le altre quattro le salviamo all'interno di self per utilizzarle più tardi. A titolo di spoiler possiamo dire che
min_xrappresenterà la minima coordinata X che il nemico portà raggiungere,max_xla massima,center_yl'altezza a cui il nostro nemico si troverà espeedla velocità con cui viaggerà tra le due X
- La prima, che conterrà il percorso al PNG con la texture del nostro nemico, verrà trasferita immediatamente al costruttore della superclasse (
- Nota come vengono aggiornate le variabili
center_xecenter_yper individuare la posizione in cui verrà piazzato inizialmente il nemico. La Y sarà ovviamente il valore ricevuto in input e sarà fissa, mentre la X è calcolata come l'ascissa del punto medio tra il massimo e il minimo raggiungibile dall'NPC (se le formula non ti convince, è purtroppo arrivata l'ora di ripassare geometria analitica)
- Come parametri riceviamo ben 5 variabili,
-
Sempre nella nostra classe
SimpleEnemyandiamo ora a definire la funzione di aggiornamento:def update(self, delta_time: float = 1 / 60, *args, **kwargs) -> None: if self.center_x < self.min_x or self.center_x > self.max_x: self.speed *= -1 self.center_x += self.speed- Questa funzione si occupa di spostare il nemico frame per frame e, dunque, di aggiornare la sua coordinata X
- Per assicurarci che non esca dai limiti definiti poco fa (
min_xemax_x) controlliamo se la sua posizione ne è al di fuori, ovvero se la sua X è minore del minimo (self.center_x < self.min_x) oppure (or) maggiore del massimo (self.center_x > self.max_x)- Qualora questo sia il caso, procediamo immediatamente a moltiplicare per -1 (
*= -1) la sua velocità (self.speed) in modo da invertire il suo moto
- Qualora questo sia il caso, procediamo immediatamente a moltiplicare per -1 (
- Una volta sicuri che spostare il nemico non lo faccia scappare dai bordi designati, procediamo a cambiarne la posizione sommando (
+=) la velocità (self.speed) alla sua X (self.center_x)
-
Visto che queste poche righe di codice definiscono pienamente un generico nemico, procediamo ora ad aggiungerne uno ad una mappa; in fondo al ramo
if self.level == 0della funzioneload_level, ad esempio, aggiungiamo:self.scene.add_sprite_list("Enemies") self.scene["Enemies"].append( SimpleEnemy(":resources:/images/enemies/slimeBlock.png", 600, 1000, 256, 3) )- Prima di tutto, visto che la mappa appena caricata (
self.scene) non prevede ancora la presenza di nemici, provvediamo ad aggiungere la lista di questi ultimi al suo interno (add_sprite_list("Enemies")) - In questa nuova lista (
self.scene["Enemies"]) andiamo dunque ad aggiungere (append) una nuova istanza di nemico (SimpleEnemy) il cui costruttore deve ricevere il percorso aslimeBlock.pngcomeenemy_image,600comemin_x,1000comemax_x,256comecenter_ye3comespeed
- Prima di tutto, visto che la mappa appena caricata (
-
Per ultimo - ma non per importanza - andiamo dunque a far sì che il gioco si ricordi di muovere mano a mano il nemico. In fondo a
update_hookaggiungi:self.scene["Enemies"].update()- Questa riga non fa altro che segnalare al gioco che, per ogni update della schermata, sarà necessario eseguire la funzione
updatedi tutti gli elementi nella lista Enemies della mappa (ovverosia i nemici)
- Questa riga non fa altro che segnalare al gioco che, per ogni update della schermata, sarà necessario eseguire la funzione
Riassumendo
Ecco il nostro esempio completo:
import arcade
from arcade import Sprite, TextureAnimation, TextureKeyframe
from platformer.platformer_base import PlatformerBase
class AnimatedPlayerSprite(Sprite):
def __init__(self, player_textures_prefix, keyframe_duration = 60):
super().__init__()
self.direction = 1
self.current_tick = 0
self.idle_texture = arcade.load_texture(player_textures_prefix + "_idle.png")
self.jumping_texture = arcade.load_texture(player_textures_prefix + "_jump.png")
keyframes = [TextureKeyframe(arcade.load_texture(player_textures_prefix + "_walk" + str(frame_id) + ".png"), keyframe_duration) for frame_id in range(0, 8)]
self.animation = TextureAnimation(keyframes)
self.update_animation()
def update_animation(self, delta_time = 1 / 60, *args, **kwargs):
if self.change_y == 0:
if self.change_x != 0:
self.current_tick += delta_time
if (self.change_x * self.direction) < 0:
self.direction *= -1
self.reverse()
curr_keyframe = self.animation.get_keyframe(self.current_tick, True)
self.texture = curr_keyframe[1].texture if self.direction == 1 else curr_keyframe[1].texture.flip_horizontally()
else:
self.current_tick = 0
self.texture = self.idle_texture if self.direction == 1 else self.idle_texture.flip_horizontally()
else:
self.texture = self.jumping_texture if self.direction == 1 else self.jumping_texture.flip_horizontally()
class SimpleEnemy(Sprite):
def __init__(self, enemy_image, min_x = 0, max_x = 0, center_y = 0, speed = 5):
super().__init__(enemy_image)
self.speed = speed
self.min_x = min_x
self.max_x = max_x
self.center_x = (min_x + max_x)/2
self.center_y = center_y
def update(self, delta_time: float = 1 / 60, *args, **kwargs) -> None:
if self.center_x < self.min_x or self.center_x > self.max_x:
self.speed *= -1
self.center_x += self.speed
class Platformer(PlatformerBase):
def __init__(self):
super().__init__()
self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
self.level = 0
self.score = 0
self.score_text = arcade.Text(f"Score: {self.score}", 30, 620)
def load_level(self):
if self.level == 0:
map_name = ":resources:tiled_maps/map2_level_1.json"
self.load_map(map_name)
self.scene.add_sprite_list("Enemies")
self.scene["Enemies"].append(
SimpleEnemy(":resources:/images/enemies/slimeBlock.png", 600, 1000, 256, 3)
)
elif self.level == 1:
map_name = ":resources:tiled_maps/map2_level_2.json"
self.load_map(map_name)
def setup_hook(self):
player_textures_prefix = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
self.set_player(AnimatedPlayerSprite(player_textures_prefix))
self.load_level()
def update_hook(self):
coin_hit_list = arcade.check_for_collision_with_list(
self.player_sprite, self.scene["Coins"]
)
for coin in coin_hit_list:
coin.remove_from_sprite_lists()
arcade.play_sound(self.collect_coin_sound)
self.score += 1
self.score_text.text = f"Score: {self.score}"
if not self.scene["Coins"]:
self.level += 1
self.load_level()
self.player_sprite.update_animation()
self.scene["Enemies"].update()
def draw_gui_hook(self):
self.score_text.draw()
def main():
Platformer.startup()
if __name__ == "__main__":
main()
Se tutto è andato a buon fine, avviando il gioco dovresti riuscire a vedere il tuo primo nemico che si muove avanti e indietro per il livello.
Challenge!
Se sei riuscito a programmare il tuo primo nemico, complimenti!
Prenditi ora un paio di minuti per rileggere il codice che hai scritto e per assicurarti di aver capito tutto, poi prova a fare queste personalizzazioni:
- Aggiungi un secondo nemico al livello;
- Crea una classe di nemico che si possa muovere in verticale (in su e in giù) e aggiungine un paio di istanze ad un livello;
- Fai in modo che toccare un nemico ti riporti all'inizio del livello (puoi resettare la posizione del player chiamando in ogni momento la funzione
self.reset_player_pos()nella classe di gioco).