Come si risolvono i tipi di basso livello a seconda dei tipi di alto livello?

5

Spesso mi imbatto in questo problema durante lo sviluppo di giochi e simulazioni. Ad esempio, sto attualmente scrivendo un'implementazione di scacchi. Lo sto usando come veicolo per sperimentare sistemi di componenti di entità e catene di responsabilità, ispirato al discorso di Brian Bucklew sull'architettura di Roguelike ( link ). In definitiva mi piacerebbe essere in grado di esprimere ogni componente comportamentale di Chess come una piccola componente incapsulata di un pezzo degli scacchi e scambiarli con un capriccio per creare rapidamente pezzi con nuovi comportamenti.

Quindi ho un Game che ha un Board , e il Board può avere un numero di Piece s ognuno dei quali può avere un numero di Component s. Non ho un'idea di che tipo sia un Piece , piuttosto codifico il loro comportamento attraverso il loro Component s; puoi inviare un messaggio "IsOwnedByPlayerOne" per determinare su quale lato si trova Piece , ad esempio:

IComponent.cs :

public interface IComponent
{
    void Handle(ref Message message);
}

Piece.cs :

public bool IsOwnedByPlayerOne()
{
    var isOwnedByPlayerOneMessage = new Message(MessageType.IsOwnedByPlayerOne);
    Handle(ref isOwnedByPlayerOneMessage);

    return isOwnedByPlayerOneMessage.GetParameter<bool>(MessageParameter.IsOwnedByPlayerOne);
}

Il problema a cui sono giunto è questo: un Component è un tipo di basso livello, ma hanno bisogno di essere consapevoli del mondo che li circonda e che possono essere raggiunti solo attraverso l'accesso a tipi di livello superiore. Ad esempio, supponiamo di inviare un messaggio "IsLegalMove" a un Piece di Rook, chiedendo se può spostarsi legalmente da a1 a a6. Dovrebbe controllare se ci sono altri Piece s tra a1 e a6, che richiede l'accesso a Board . Ma Board stesso contiene Piece che a sua volta contiene Component , quindi concettualmente il design sta diventando un po 'anonimo.

Potrei fornire come campo un Piece o Component di accesso a Board . Oppure potrei aggiungere Board come parametro al metodo I Component '' s Handle() . Non mi sento bene per nessuna di queste soluzioni, ma non riesco a capire perché. Quale sarebbe meglio allineato con i principi del software di qualità? O ci sono altri che consiglieresti?

    
posta Jansky 26.05.2018 - 14:50
fonte

5 risposte

3

inspired by Brian Bucklew's talk on Roguelike architecture

Che cosa ha fatto Bucklew prende tutti i parametri e i metodi che normalmente incorporiamo in un'interfaccia (definendo così il mini linguaggio utilizzato dagli oggetti client per comunicare con i loro oggetti di servizio) e li ha inseriti in un evento. Quindi ora l'unica cosa nelle interfacce tra gli oggetti è l'unico metodo che accetta gli eventi.

EppureeccoticonIsOwnedByPlayerOne().Questononèunevento.Manondeviseguireilsuometodo.

UltimatelyI'dliketobeabletoexpresseachbehaviouralcomponentofChessasasmall,encapsulatedcomponentofachesspieceandswaptheseoutatawhimtoquicklycreatepieceswithnewbehaviours.

PuoifarloanchenellelinguedellavecchiascuolaOOP,senza facendo la costruzione in xml o scrivi il tuo caricatore di classe . Piuttosto, cerca sempre l'ereditarietà, invece impara a preferisci la composizione e la delega .

Lascia che ti dica come ho fatto la mia regina nel il mio torneo di scacchi . Era solo un pezzo come tutti gli altri. Ma quando arrivò il momento di generare le sue mosse, non poteva esserne disturbata. Ha appena chiesto alla torre e al vescovo cosa potevano fare se fossero al suo posto.

Non l'ho fatto ereditando le classi Rook o Bishop. Ho composto la mia regina con loro. Erano iniettati in lei quando era nata. La sua conoscenza di loro era controllata. Non sapeva nemmeno quale fosse. Sapeva solo che aveva una squadra di pezzi in grado di decidere la sua lista delle mosse. Ha delegato il lavoro a loro. Tutto quello che ha fatto è stato trasformare le loro liste in una lista.

Questo significava che gli elefanti Seirawan potrebbero essere aggiunti al gioco solo iniettando il cavaliere.

The issue I've come to is this: a Component is very much a low-level type, but they need awareness of the world around them which can only be achieved through access to higher-level types.

Questo è un problema diverso. Concettualmente è un po 'difficile. Mi ricorda la relatività. Lascia che ti chieda questo: sai dove sei? Come lo sai? Se ti drogassi e prendessi te e il tuo letto e ti facessi galleggiare in un lago, come sapresti dove sei? Qualcosa deve dirti che stai galleggiando in un lago.

Ecco come ho risolto questo problema. Ho detto ai pezzi dove erano. In questo modo non devono ricordare e non devono chiedere.

Per visualizzare un pezzo:

board[rank, file].display(rank, file);

Per generare mosse legali di pezzi:

moves.addAll( board[rank, file].listMoves(board, rank, file) );

Questo segue un principio che ha pochi nomi, ma mi piace chiamarlo "Dillo, non chiedere" .

Il design ha permesso all'utilizzo del codice di trattare i pezzi in modo polimorfico (la parte migliore di OOP) e funzionalmente. Il che significava che poteva semplicemente fare un loop della board, che era una semplice matrice 2D di riferimenti a pezzi e alcuni stati di gioco. Era necessario aggiungere un oggetto null pezzo che non mostrava nulla, non offriva mosse, che chiunque poteva catturare e che non bloccava movimento.

For example, say I send a "IsLegalMove" message to a Rook Piece, asking whether it can legally move from a1 to a6. It would need to check whether there are any other Pieces between a1 and a6, which requires access to the Board. But the Board itself contains the Piece which in turn contains the Component, so conceptually the design is getting a bit loopy.

Non inviare messaggi "IsLegalMove". Invia

newMoveList = board("a2").filterMoves(board, rooksMoveList)

E lascia a2 sfoltire le mosse illegali per te con la sua incredibile capacità di sapere se è amico, nemico o vuoto. Lo farai non solo per il quadrato a2 ma per tutti i quadrati tra a1 e a6. Lo fai per ogni mossa che la torre potrebbe fare. Oltre a semplificare la programmazione, questo garantisce che una torre circondata da pezzi amichevoli o bordi del tavolo non mangi il tempo inutilmente.

I could give either Piece or Component access to Board as a field. Or I could add Board as a parameter to IComponent'''s Handle() method. I don't feel great about either of these solutions but can't put my finger on why. Which one would be better aligned with quality software principles? Or are there others you'd recommend?

Sentirsi bene con un approccio arriva solo con il tempo. Non significa che sia giusto. Solo che ti sei abituato. Dopo decenni a questo, mi sento davvero a mio agio nel minimizzare ciò che sa di cosa. Se un pezzo non deve sapere dove si trova quando non lo sto usando, perché farlo ricordare? Lascia fare alla lavagna.

    
risposta data 26.05.2018 - 21:18
fonte
3

say I send a "IsLegalMove" message to a Rook Piece, asking whether it can legally move from a1 to a6

Dato che un pezzo non può sapere da solo se una mossa è legale, raccomanderei contro questo disegno. Un pezzo (conoscendo il suo tipo, colore e posizione) può conoscere le regole in cui può essere spostato dalla sua posizione corrente su una tavola vuota, quindi puoi dargli un metodo virtuale GetListOfPotentialMoves per restituire l'elenco delle mosse legali su un scheda vuota (gli unici parametri che questo metodo richiede sono la larghezza e l'altezza della scheda). Non dovrebbe solo restituire la mossa come una coordinata finale, ma anche le coordinate intermedie per ogni mossa (quelle che devono essere vuote per una mossa legale).

L'oggetto Board , tuttavia, può generare l'elenco di mosse legali, chiamando Piece.GetListOfPotentialMoves e filtrando i risultati in base alle regole (che le coordinate intermedie devono essere vuote e la coordinata di destinazione deve essere vuota o contenere un pezzo di un colore diverso).

Si noti che in questo progetto, il pezzo non deve conoscere il Board , e Board non deve sapere quale tipo di pezzo individuale ha a che fare.

    
risposta data 26.05.2018 - 15:48
fonte
2

Non sono sicuro che questo sia un buon progetto, ma ho costruito un'implementazione di "apprendimento" molto simile.

La mia soluzione era passare la scheda come parametro a IsLegalMove , e il Pezzo quindi chiama la Commissione per fare in modo che sia una mossa legale. La combinazione dei due ha la conoscenza necessaria; per esempio, un Rook sa che può percorrere file e colonne (prima controlla, nella classe Rook che è derivata da Piece), e poi la Board sa (secondo livello di controllo) che i pezzi non possono muoversi fuori dal tabellone, non possono saltare sopra altri pezzi, e può solo prendere pezzi dell'avversario.

L'ho esteso in modo che lo stesso motore di gioco possa gestire molti giochi; per esempio Camelot, Halma e Shogi - quali cambiamenti sono solo le regole che la bacheca applica a ciò che è permesso (quindi ricavo schede specifiche per il gioco dalla classe del consiglio generale).

Penso che questo sia chiamato Iniezione delle dipendenze (perché stai 'iniettando' nel Pezzo l'oggetto necessario (Board) per gestire la sua dipendenza dai suoi dintorni), ma non sono sicuro se questo caso si qualifica per il termine DC.

    
risposta data 26.05.2018 - 15:28
fonte
0

In generale, devi specificare sia WorldState che Entity che desideri gestire per decidere cosa fare.

Poiché Entity non agirà senza chiedere conferma a qualcuno che conosce il GameState corretto, aggiungere un puntatore a quest'ultimo è uno spreco, oltre a inibire la copia semplice.

Non dovresti salvare Entity in modo che agisca anche su GameState , poiché cambia a seconda del metodo attivo, ed è a questo che servono i parametri del metodo.

Sia che chiami un metodo che attiva il tipo di componente, o chiami una funzione membro virtuale del componente, è molto importante la tua decisione.

    
risposta data 26.05.2018 - 16:18
fonte
0

Se ci sono un gioco, una scacchiera e un pezzo, allora:

  • Una commissione ha campi, che ha i pezzi.
  • I pezzi hanno attributi (colore, forma, dimensione, possono saltare sopra pezzi)
  • I pezzi possono verificare tramite delta (orizzontale, verticale, diagonale) se una mossa è valida.
  • il gioco ha regole di gioco
  • il gioco chiede / controlla (esegue) le regole se una mossa di un pezzo è valida
risposta data 26.05.2018 - 17:04
fonte