2010-07-16 12 views
14

Ho imparato C# durante l'estate e ora mi sento come fare un piccolo progetto da quello che ho fatto finora. Ho deciso una sorta di gioco di avventura basato su testo.Interrogazione relativa alla progettazione di giochi di avventura di testo basati su classi.

La struttura di base del gioco riguarderà un numero di settori (o stanze). All'ingresso in una stanza, verrà fornita una descrizione e un numero di azioni e azioni che potresti intraprendere; la capacità di esaminare, raccogliere, usare cose in quella stanza; possibilmente un sistema di battaglia, ecc. ecc. Un settore può essere collegato fino a 4 altri settori.

In ogni caso, scarabocchiare idee su carta su come progettare il codice per questo, mi sto grattando la testa sulla struttura di parte del mio codice.

Ho deciso una classe giocatore e una classe 'livello' che rappresenta un livello/dungeon/area. Questa classe di livello consisterebbe in un certo numero di "settori" interconnessi. In qualsiasi momento, il giocatore sarà presente in un certo settore del livello.

Quindi, ecco la confusione:

Logicamente, ci si aspetterebbe un metodo come player.Move(Dir d)
Tale metodo dovrebbe cambiare il campo 'settore corrente' nell'oggetto di livello. Ciò significa che la classe Player deve conoscere la classe Livello. Hmmm. E livello potrebbe dover manipolare il lettore oggetto (ad es. Il giocatore entra nella stanza, un'imboscata da qualcosa, perde qualcosa dal magazzino.) Così ora livello ha anche bisogno di mantenere un riferimento alla Player oggetto?

Questo non è piacevole; tutto ciò che deve contenere un riferimento a tutto il resto.

A questo punto mi sono ricordato di aver letto dei delegati del libro che sto usando. Sebbene conosca i puntatori di funzione del C++, il capitolo sui delegati è stato presentato con esempi con una sorta di punto di vista della programmazione 'evento basato', con il quale non avevo molta illuminazione.

Questo mi ha dato l'idea di progettare le classi come segue:

giocatore:

class Player 
{ 
    //... 

    public delegate void Movement(Dir d); //enum Dir{NORTH, SOUTH, ...} 

    public event Movement PlayerMoved; 

    public void Move(Dir d) 
    {   
     PlayerMoved(d); 

     //Other code... 
    } 

} 

Livello:

class Level 
{ 
    private Sector currSector; 
    private Player p; 
    //etc etc... 

    private void OnMove(Dir d) 
    { 
     switch (d) 
     { 
      case Dir.NORTH: 
       //change currSector 
       //other code 
       break; 

       //other cases 
     } 
    } 

    public Level(Player p) 
    { 
     p.PlayerMoved += OnMove; 
     currSector = START_SECTOR; 
     //other code 
    } 

    //etc... 
} 

È questo un modo va bene per fare questo?
Se il capitolo dei delegati non fosse presentato come era, non avrei pensato di utilizzare tali "eventi". Quindi, quale sarebbe un buon modo per implementarlo senza utilizzare i callback?

Ho l'abitudine di fare i messaggi altamente dettagliate ... scusate v__v

+4

+1 per il post altamente dettagliato – Task

+1

+1 per un post altamente dettagliato. ;-) –

+2

Hai bisogno di chiedertelo, è davvero d'aiuto chiamare il tuo enum 'Dir'? Il fatto che fosse necessario un commento dovrebbe darti un suggerimento. – ChaosPandion

risposta

5

Che dire di Classe A 'Game' tale da ostacolare la maggior parte delle informazioni come un giocatore e una sala corrente. Per un'operazione come lo spostamento del giocatore, la classe di gioco potrebbe spostare il giocatore in una stanza diversa in base alla mappa del livello della stanza.

La classe di gioco gestirà tutte le interazioni tra i vari componenti dei giochi.

L'utilizzo di eventi per qualcosa di simile porta il pericolo che i tuoi eventi si ingarbugliano. Se non stai attento, finirai con gli eventi che si spareranno a vicenda e trabatteranno il tuo stack, il che porterà a flag per disattivare gli eventi in circostanze particolari e un programma meno comprensibile.

UDPATE:

per rendere il codice più gestibile, si potrebbe modellare alcune delle interazioni tra le classi principali come classi stesse, come ad esempio una classe di lotta. Utilizza le interfacce per consentire alle tue classi principali di eseguire determinate interazioni. (Nota che mi sono preso la libertà di inventare alcune cose che potresti non volere nel tuo gioco).

Ad esempio:

// Supports existance in a room. 
interface IExistInRoom { Room GetCurrentRoom(); } 

// Supports moving from one room to another. 
interface IMoveable : IExistInRoom { void SetCurrentRoom(Room room); } 

// Supports being involved in a fight. 
interface IFightable 
{ 
    Int32 HitPoints { get; set; } 
    Int32 Skill { get; } 
    Int32 Luck { get; } 
} 

// Example class declarations. 
class RoomFeature : IExistInRoom 
class Player : IMoveable, IFightable 
class Monster : IMoveable, IFightable 

// I'd proably choose to have this method in Game, as it alters the 
// games state over one turn only. 
void Move(IMoveable m, Direction d) 
{ 
    // TODO: Check whether move is valid, if so perform move by 
    // setting the player's location. 
} 

// I'd choose to put a fight in its own class because it might 
// last more than one turn, and may contain some complex logic 
// and involve player input. 
class Fight 
{ 
    public Fight(IFightable[] participants) 

    public void Fight() 
    { 
    // TODO: Logic to perform the fight between the participants. 
    } 
} 

Nella tua domanda, è identificato il fatto che si avrebbe molte classi che devono sapere gli uni degli altri se bloccato qualcosa come un metodo Move sulla tua classe Player. Questo perché qualcosa come una mossa non appartiene né a un giocatore né a una stanza - la mossa influenza entrambi gli oggetti reciprocamente. Modellando le "interazioni" tra gli oggetti principali è possibile evitare molte di queste dipendenze.

+0

Per lo spostamento, avrei un metodo in Game che ha: new_loc = player.GetNewLocation (direction), level.CheckLocation (new_loc), player.SetLocation (new_loc), level.SetPlayerLocation (player). Inoltre, renderei l'oggetto Game un oggetto scriptato (come Lua, Python, ecc.). – Skizz

+0

Quando arrivai con l'implementazione di cui sopra, improvvisamente sentii che molte cose potevano essere implementate attraverso tali eventi e cercavo di ricordarmi di non essere portato via. Immagino sia solo l'effetto dell'uso di un nuovo 'giocattolo'. – Rao

+0

Se hai seguito questo metodo di progettazione, la tua classe di gioco non sarebbe stata enorme, con tutti i tipi di dati e funzioni che erano collegati l'uno all'altro solo in virtù del fatto di essere nella stessa applicazione (Gioco)? –

0

Quindi, in primo luogo, questo è un buon modo per fare questo?

Assolutamente!

In secondo luogo, se il capitolo è stato delegato non ha presentato il modo in cui era, avrei non ho pensato di utilizzare tali 'eventi'. Quindi, quale sarebbe il modo migliore per implementare questo senza utilizzare i callback ?

io conosco un sacco di altri modi per implementare questo, ma nessun altro buon modo qualsiasi, senza un qualche tipo di meccanismo di callback. IMHO è il modo più naturale per creare un'implementazione disaccoppiata.

2

Suona come uno scenario Spesso utilizzo una classe di comando o una classe di servizio per. Ad esempio, potrei creare una classe MoveCommand che esegua le operazioni e i coordinamenti su e tra Livelli e Persone.

Questo schema ha il vantaggio di rafforzare ulteriormente il Principio di Responsabilità Unica (SRP). SRP dice che una classe dovrebbe avere solo una ragione per cambiare. Se la classe Person è responsabile dello spostamento avrà indubbiamente più motivi per cambiare. Rompendo la logica di un Move off nella sua stessa classe, è meglio incapsulato.

Esistono diversi modi per implementare una classe Command, ognuno dei quali adatta meglio a diversi scenari.le classi di comando potrebbero avere un metodo che accetta tutti i parametri necessari eseguire:

public class MoveCommand { 
    public void Execute(Player currentPlayer, Level currentLevel) { ... } 
} 

public static void Main() { 
    var cmd = new MoveCommand(); 
    cmd.Execute(player, currentLevel); 
} 

Oppure, a volte lo trovo più semplice e flessibile, per utilizzare le proprietà per l'oggetto di comando, ma rende più facile per il codice client per uso improprio la classe dimenticando per impostare le proprietà - ma il vantaggio è che si ha la stessa firma funzione per l'esecuzione su tutte le classi di comando, in modo da poter fare un'interfaccia per quel metodo e di lavorare con i comandi astratti:

public class MoveCommand { 
    public Player CurrentPlayer { get; set; } 
    public Level CurrentLevel { get; set; } 
    public void Execute() { ... } 
} 

public static void Main() { 
    var cmd = new MoveCommand(); 
    cmd.CurrentPlayer = currentPlayer; 
    cmd.CurrentLevel = currentLevel; 
    cmd.Execute(); 
} 

Infine, potresti fornire i parametri come argomenti del costruttore per la classe Command, ma io dimenticherò quel codice.

In ogni caso, trovo che usare Comandi o Servizi è un modo molto potente per gestire operazioni, come Move.

+0

+1 compagno, questo funzionerebbe abbastanza bene, e si finirebbe con il codice che era molto uniforme e facile da produrre/mantenere. –

+0

Grazie. Funziona bene per me implementare funzionalità nei progetti MVC e quello che mi piace davvero è come supporta anche Open-Closed Principal. Cambio comportamento implementando una nuova classe Command, piuttosto che cambiando le implementazioni dei metodi in una classe di servizio [solitamente invasa]. –

+0

Ho riflettuto su questa risposta. Quali sono alcuni vincoli sul design delle classi Player, Level usando questo pattern? Idealmente, che tipo di metodi dovrebbe chiamare MoveCommand.Execute? – Rao

1

Per un gioco basato sul testo, si avrà quasi certamente un oggetto CommandInterpretor (o simile), che valuta i comandi digitati dell'utente. Con quel livello di astrazione, non devi implementare ogni azione possibile sul tuo oggetto Player. Il tuo interprete potrebbe inviare alcuni comandi digitati al tuo oggetto Player ("mostra inventario"), alcuni comandi all'oggetto Sector attualmente occupato ("elenco uscite"), alcuni comandi all'oggetto Level ("move player North"), e alcuni comandi a oggetti speciali ("attacco" potrebbe essere inserito in un oggetto CombatManager).

In questo modo, l'oggetto Player diventa più simile al Carattere, e CommandInterpretor è più rappresentativo dell'attuale giocatore umano seduto alla tastiera.

+0

Tutto è ancora nella fase di scarabocchi su carta, e quindi mediterò profondamente sulla tua risposta e su quella di qstarin :) – Rao

1

Evitare di essere emotivamente o intellettualmente impantanati in quello che è il modo "giusto" di fare qualcosa. Concentrarsi invece su facendo. Non attribuire troppo valore al codice che hai già scritto, perché potrebbe essere necessario modificarlo in tutto o in parte per supportare le attività che desideri eseguire.

IMO ci sono troppe energie spesi per schemi e tecniche interessanti e tutto questo jazz. Basta scrivere un codice semplice per fare la cosa che vuoi fare.

Il livello "contiene" tutto all'interno di esso. Puoi iniziare da lì. Il livello non dovrebbe necessariamente guidare tutto, ma tutto è nel livello.

Il giocatore può muoversi, ma solo entro i confini del livello. Pertanto, il giocatore deve interrogare il livello per vedere se una direzione di movimento è valida.

Il livello non sta prendendo oggetti dal giocatore, né il livello che infligge danno. Altri oggetti nel livello stanno facendo queste cose. Questi altri oggetti dovrebbero cercare il giocatore, o forse dire della vicinanza del giocatore, e quindi possono fare ciò che vogliono direttamente al giocatore.

È normale che il livello "possieda" il giocatore e che il giocatore abbia un riferimento al suo livello. Questo "ha senso" da una prospettiva OO; ti trovi sul Pianeta Terra e puoi influenzarlo, ma ti sta trascinando per l'universo mentre stai scavando buchi.

Fai cose semplici. Ogni volta che qualcosa si complica, scopri come renderlo semplice. Il codice semplice è più facile da lavorare ed è più resistente ai bug.

+0

Lo terrò a mente. – Rao

Problemi correlati