Come devo tenere traccia di quale mostro è il primo in una partita di Tower Defense, ed è giusto creare un attributo extra solo per questo scopo?

3

Quindi sto creando un gioco di Tower Defense e voglio avere diversi modi per le torri per dare la priorità ai loro bersagli (sto usando Python ma sto cercando una risposta generica al design):

class TargetPriority(enum.Enum):
    LOW_HEALTH = 1
    HIGH_HEALTH = 2
    FIRST = 3
    LAST = 4
    CLOSEST = 5


class Tower(Entity):
    def __init__(self, *args, **kwargs, target_priority=TargetPriority.FIRST):
        super().__init__(*args, **kwargs)
        self.target_priority = target_priority

    def find_target(self, all_monsters):
        sort_functions = {
            TargetPriority.LOW_HEALTH: lambda monster: monster.health,
            TargetPriority.HIGH_HEALTH: lambda monster: -monster.health,
            TargetPriority.FIRST: ???,
            TargetPriority.LAST: ???,
            TargetPriority.CLOSEST: self.distance_to,
        }
        func = sort_functions[self.target_priority]
        return next(sorted(all_monsters, key=func), None)

Come puoi vedere, non sono sicuro di come ottenere il mostro "primo" o "ultimo" dall'ondata di mostri. Il modo in cui i miei mostri funzionano in questo momento è ottenere un oggetto Checkpoint e una volta raggiunto il checkpoint, chiamano il metodo check_in() per ottenere nuove istruzioni:

class Monster(Entity):
    def __init__(self, *args, **kwargs, health, checkpoint):
        super().__init__(*args, **kwargs)
        self.health = health
        self.checkpoint = checkpoint

    def update(self, dt):
        travel = self.velocity / 1000 * dt
        remaining_distance = self.distance_to(self.checkpoint)
        if travel.length < remaining_distance:
            self.position += travel
        else:
            self.checkpoint.check_in(self)

Il metodo Checkpoint.check_in(monster) può richiamare qualsiasi callback assegnata al checkpoint, di solito modifica semplicemente il checkpoint del mostro al prossimo checkpoint, ma può anche fare qualcos'altro come togliere una vita dal giocatore e rimuovere il mostro (il l'ultimo checkpoint lo fa sempre). Posso cambiare il modo in cui funzionano questi checkpoint (o come i mostri si muovono in generale) se richiesto da una soluzione "ottimale".

Ecco alcune cose che ho considerato:

  • Non posso usare l'ordine dei mostri nella lista all_monsters perché alcune torri potrebbero rallentare alcuni mostri, cambiando così il loro ordine
  • Per la stessa ragione, non posso usare nessun altro tipo di indicizzazione
  • Non posso usare la posizione dei mostri sulla mappa perché non c'è modo di sapere da che parte sta andando il mostro o dove sono i checkpoint
  • Non posso usare la distanza dei mostri per i checkpoint perché i mostri possono già trovarsi a diversi checkpoint

Finora l'unica soluzione effettiva che ho potuto trovare era di memorizzare un total_distance_travelled per ogni mostro, quindi in update() avrei:

self.position += travel
self.total_distance_travelled += travel.length

Ma ci sono due ragioni per le quali esito:

  • Non è una prova di fallimento al 100%: anche se non è attualmente possibile, cosa succede se avrò qualcosa di simile a torri che teletrasporteranno i mostri al loro precedente checkpoint?
  • Sto aggiungendo un attributo di istanza solo per il solo motivo che le torri sono in grado di ordinare questi mostri

Mi preoccupo troppo, o c'è una soluzione migliore che mi manca?

    
posta Markus Meskanen 12.12.2017 - 01:30
fonte

2 risposte

5

Quello che hai è la soluzione più pratica per come la vedo io:

self.total_distance_travelled += travel.length

Per il primo e l'ultimo targeting devi avere un concetto di progress e non dovrebbe essere così complicato da codificare. Se hai teletrasportatori che possono spingere indietro i mostri, vengono respinti e il loro avanzamento dovrebbe essere decrementato, e dovresti essere in grado di calcolare il percorso e la lunghezza di quel percorso per il teletrasporto e semplicemente diminuire total_distance_travelled da quel di lunghezza.

It's not 100% fail proof: although not currently possible, what if I will have something like towers that teleport monsters back to their previous checkpoint?

Naturalmente, se teletrasporta i mostri all'indietro di una distanza casuale, ad esempio, potrebbe essere necessario effettuare il path-finding per capire dove teletrasportarli o utilizzare questo sistema di checkpoint. Tuttavia, puoi pensare che tipo " Quanti passi / piedi / metri / pixel all'indietro ho spostato il mostro? " e solo diminuire di total_distance_travelled di quella quantità.

I'm adding an instance attribute just for the sole reason of towers being able to sort these monsters

Non preoccuparti, in quanto dovrebbe essere perfettamente normale nei TD che hanno priorità di targeting per il primo / ultimo. Se sei preoccupato per l'efficienza qui, la priorità più grande di solito è accelerare la ricerca di mostri in prossimità della torre (dal momento che può soffrire di problemi di scalabilità se si lancia una barca di mostri sulla mappa in una sola volta), per cui partizionare i mostri in possono essere utili celle di griglia vecchie e semplici: link

Un'alternativa: tempo di raggiungere l'obiettivo finale

L'unica alternativa che posso pensare è implementare un tipo di funzione di distance_to_goal() molto scalabile che puoi chiamare sui mostri al volo, potenzialmente ogni frame, che calcola la distanza totale per raggiungere l'obiettivo finale (non il prossimo checkpoint ). Quelli con il valore minimo sarebbero le prime priorità, quelli con il valore massimo sarebbero gli ultimi. Sarebbe molto robusto, ma sposta molta sfida verso l'ottimizzazione e si presenta con un modo molto rapido per calcolare la distanza dall'obiettivo e vedere se è possibile utilizzare calcoli più economici a seconda della situazione. La maggior parte dei TD, penso, usa solo quello che stai facendo.

Se lo provi, potresti combinarlo con quello che stai facendo ora, tranne girare total_distance_travelled su qualcosa come total_distance_remaining che viene decrementato mentre il mostro avanza verso l'obiettivo finale finale dove sottraggono una vita dal giocatore e alla fine portano al game over. Quindi potresti ricalcolarlo se il percorso cambia per qualche motivo o se il mostro viene teletrasportato.

In realtà non ci siamo mai concentrati su priorità di targeting prima (ho appena finito di utilizzare il tipo pragmatico di soluzione che stai già utilizzando), ma la tecnica di cui sopra utilizza " distanza rimanente all'obiettivo finale " sarebbe sicuramente il più robusto se puoi calcolarlo e aggiornarlo abbastanza efficientemente.

Mi è venuto in mente anche che se lo fai in questo modo potresti essere in grado di modellare meglio le intenzioni del giocatore se anche calcoli la velocità del mostro e dividi quella distanza rimanente dalla velocità del mostro per ottenere, "time to reach final goal" , perché il giocatore probabilmente vorrebbe first targeting prioritario per bersagliare mostri più veloci se raggiungeranno la fine prima, anche se iniziano un po 'indietro.

Un'idea divertente con questo: potresti cercare il mostro con il tempo più basso per raggiungere l'obiettivo finale e se è solo a pochi secondi, potresti accelerare un cuore pulsante o iniziare a girare lo schermo in rosso o sfumare in una musica spaventosa o qualcosa del genere questo, diventando più intenso come il "momento minimo per raggiungere l'obiettivo finale" funziona verso zero. Per i test potresti utilizzare una funzione di sviluppo per attirare il tempo residuo su ciascun mostro per assicurarti che venga calcolato correttamente in ogni momento.

    
risposta data 12.12.2017 - 13:29
fonte
2

Se hai percorsi di ramificazione, non non hai un singolo, obiettivo, "Primo" mostro nell'onda (stesso "Ultimo"). Ci sono un paio di cose che probabilmente giocano bene:

Ignora la posizione attuale e conta solo l'ordine di spawn.

  • Pro: implementazione molto semplice, tieni solo l'ordine all_monsters
  • Contro: le torri pushback non influiscono sulla priorità come vorrai.

Fai un topologico ordina dei checkpoint, ordina per checkpoint e distanza per checkpoint.

  • Pro: dà un ordine totale sensibile su ogni sottografo lineare
  • Con: fondamentalmente favorisce arbitrariamente un percorso piuttosto che un altro, non ovvio al giocatore perché

Una variazione del tipo topologico sarebbe calcolare un "percorso minimo di distanza per uscire" per ogni punto di controllo (una volta, quando si imposta il livello) e aggiungerlo alla distanza dal punto di controllo. Quindi il giocatore ha una migliore intuizione di "questi potrebbero prendere quella scorciatoia, quindi devono morire per primi", anche se solo una frazione in realtà

Potresti anche fare entrambe le cose

def find_target(self, all_monsters):
    counter = 0
    def first(monster):
        counter += 1
        return -counter
    def last(monster):
        counter += 1
        return counter
    def compare_topological(left, right):
        if (comparable(left.checkpoint, right.checkpoint)):
            return (checkpoint_order(left.checkpoint), left.distance_to(left.checkpoint)) < 
                   (checkpoint_order(right.checkpoint), right.distance_to(right.checkpoint))
        else:
            return 0 # incomparable

    sort_functions = {
        TargetPriority.LOW_HEALTH: lambda monster: monster.health,
        TargetPriority.HIGH_HEALTH: lambda monster: -monster.health,
        TargetPriority.FIRST_SPAWNED: first,
        TargetPriority.LAST_SPAWNED: last,
        TargetPriority.FIRST_TOPOLOGICAL: functools.cmp_to_key(compare_topological),
        TargetPriority.CLOSEST: self.distance_to,
    }
    func = sort_functions[self.target_priority]
    return next(sorted(all_monsters, key=func), None)
    
risposta data 12.12.2017 - 10:28
fonte

Leggi altre domande sui tag