2010-07-16 12 views
14

He estado aprendiendo C# durante el verano y ahora tengo ganas de hacer un pequeño proyecto de lo que he hecho hasta ahora. Me decidí por una especie de juego de aventuras basado en texto.Consulta sobre el diseño del juego de aventuras de texto basado en clases.

La estructura básica del juego implicará tener una serie de sectores (o salas). Al ingresar a una sala, se generará una descripción y se tomarán una serie de acciones; la capacidad de examinar, recoger, usar cosas en esa habitación; posiblemente un sistema de batalla, etc. etc. Un sector puede estar conectado a otros 4 sectores.

De todos modos, garabateando ideas en papel sobre cómo diseñar el código para esto, me estoy rascando la cabeza sobre la estructura de parte de mi código.

He decidido una clase de jugador y una clase de "nivel" que represente un nivel/mazmorra/área. Esta clase de nivel consistiría en una serie de 'sectores' interconectados. En cualquier momento dado, el jugador estaría presente en un sector determinado en el nivel.

Así que aquí está la confusión:

Lógicamente, uno esperaría un método como player.Move(Dir d)
Dicho procedimiento debe cambiar el campo 'sector actual' en el objeto de nivel. Esto significa que la clase Player necesitaría saber sobre la clase Level. Hmmm. Y Nivel puede tener que manipular el jugador objeto (por ejemplo. El jugador entra en la habitación, emboscado por algo, pierde algo de inventario.) Así que ahora Nivel también tiene que contener una referencia a la jugador objeto?

Esto no se siente bien; todo tiene que contener una referencia a todo lo demás.

En este punto recordé haber leído acerca de los delegados del libro que estoy usando. Aunque conozco los indicadores de función de C++, el capítulo sobre delegados se presentó con ejemplos con una especie de punto de vista de programación "basado en eventos", con el que no tenía mucha información.

Eso me dio la idea de diseñar las clases de la siguiente manera:

jugador:

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... 
    } 

} 

Nivel:

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... 
} 

¿Es esta una manera bien para hacer esto?
Si el capítulo de delegados no se presentó tal como era, no habría pensado en usar tales 'eventos'. Entonces, ¿cuál sería una buena forma de implementar esto sin usar callbacks?

que tienen la costumbre de hacer mensajes muy detallados ... lo siento v__v

+4

+1 para una publicación muy detallada – Task

+1

+1 para una publicación muy detallada. ;-) –

+2

Tienes que preguntarte, ¿realmente ayuda a nombrar a tu enum 'Dir'? El hecho de que se requiera un comentario debería darle una pista. – ChaosPandion

Respuesta

5

¿Qué pasa con la clase de un 'juego' que tengan la mayoría de la información como un jugador y una sala actual. Para una operación como mover al jugador, la clase Juego podría mover al jugador a una habitación diferente según el mapa de nivel de la sala.

La clase de juego gestionaría todas las interacciones entre los diversos componentes de los juegos.

Usar eventos para algo como esto trae el peligro de que sus eventos se enreden. Si no tiene cuidado, terminará con eventos que disparan entre sí y desbordan su pila, lo que dará lugar a banderas para desactivar eventos en circunstancias especiales, y un programa menos comprensible.

UDPATE:

para hacer el código más manejable, se puede modelar algunas de las interacciones entre las principales clases como a sí mismos, clases, tales como una clase de Lucha. Use interfaces para permitir que sus clases principales realicen ciertas interacciones. (Tenga en cuenta que me he tomado la libertad de inventar algunas cosas que puede que no desee en su juego).

Por ejemplo:

// 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. 
    } 
} 

En su pregunta, se identificó el hecho de que tendría muchas clases que tienen que saber el uno del otro si usted se pegó algo así como un método Move en su clase Player. Esto se debe a que algo como un movimiento no pertenece ni a un jugador ni a una habitación; el movimiento afecta a ambos objetos mutuamente. Al modelar las "interacciones" entre los objetos principales, puede evitar muchas de esas dependencias.

+0

Para mover, tendría un método en el juego que sí: new_loc = player.GetNewLocation (dirección), level.CheckLocation (new_loc), player.SetLocation (new_loc), level.SetPlayerLocation (player). También haría que el objeto del juego sea un objeto con guiones (como Lua, Python, etc.). – Skizz

+0

Cuando se me ocurrió la implementación anterior, de repente sentí que se podían implementar muchas cosas a través de tales eventos y traté de recordarme a mí misma que no debía dejarme llevar. Supongo que es solo el efecto de usar un nuevo "juguete". – Rao

+0

Si siguieras este método de diseño, ¿tu clase de Juego no terminaría siendo enorme, con todo tipo de datos y funciones que solo estaban relacionados entre sí por el hecho de estar en la misma aplicación (Juego)? –

0

Por lo tanto, en primer lugar, ¿es esta una forma correcta de hacer esto?

¡Absolutamente!

En segundo lugar, si el capítulo delegado se no presenta la forma en que estaba, me no he pensado en utilizar este tipo de eventos ''. Entonces, ¿cuál sería una buena forma de implementar sin usar las devoluciones de llamada ?

Conozco a un montón de otras maneras de implementar esto, pero no hay ningún otro buena manera sin algún tipo de mecanismo de devolución de llamada. En mi humilde opinión, es la forma más natural de crear una implementación desacoplada.

2

Suena como un escenario para el que a menudo uso una clase de comando o una clase de servicio. Por ejemplo, podría crear una clase MoveCommand que realice las operaciones y coordinaciones en y entre Niveles y Personas.

Este patrón tiene la ventaja de reforzar aún más el principio de responsabilidad única (SRP). SRP dice que una clase solo debería tener una razón para cambiar. Si la clase Persona es responsable de moverlo, sin duda tendrá más de un motivo para cambiar. Al romper la lógica de un Move off en su propia clase, queda mejor encapsulado.

Existen varias formas de implementar una clase de comando, cada una de las cuales encaja mejor en diferentes escenarios.clases de comando podría tener un método que toma todos los parámetros necesarios ejecutar:

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

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

O, a veces me resulta más sencillo, y flexible, para utilizar las propiedades del objeto de comando, pero hace que sea más fácil para el código de cliente a un mal uso la clase por el olvido para establecer las propiedades - pero la ventaja es que usted tiene la misma firma de función para ejecutar en todos los códigos de comando, para que pueda hacer una interfaz para que el método y el trabajo con los comandos abstractos:

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(); 
} 

por último, podrías proporcionar los parámetros como argumentos de constructor a la clase Command, pero renunciaré a ese código.

En cualquier caso, considero que usar Comandos o Servicios es una forma muy poderosa de manejar operaciones, como Mover.

+0

+1 amigo, esto funcionaría muy bien, y terminarías con un código que era muy uniforme y fácil de producir/mantener. –

+0

Gracias. Funciona bien para mí la implementación de la funcionalidad en proyectos de MVC, y lo que realmente me gusta es cómo también es compatible con Open-Closed Principal. Cambio el comportamiento mediante la implementación de una nueva clase de comando, en lugar de cambiar las implementaciones de métodos en una clase de servicio [generalmente demasiado grande]. –

+0

He estado reflexionando sobre esta respuesta. ¿Cuáles son algunas restricciones en el diseño del jugador, las clases de nivel que usan este patrón? Idealmente, ¿qué tipo de métodos debería usar la llamada MoveCommand.Execute? – Rao

1

Para un juego basado en texto, es casi seguro que tendrá un objeto CommandInterpretor (o similar), que evalúa los comandos escritos por el usuario. Con ese nivel de abstracción, no tiene que implementar todas las acciones posibles en su objeto Player. Su intérprete puede enviar algunos comandos escritos a su objeto Player ("mostrar inventario"), algunos comandos al objeto Sector actualmente ocupado ("lista sale"), algunos comandos al objeto Level ("mover jugador al Norte"), y algunos comandos a objetos especiales ("ataque" podría ser empujado a un objeto CombatManager).

De esta forma, el objeto Player se parece más al Character, y CommandInterpretor es más representativo del jugador humano real sentado en el teclado.

+0

Todo está todavía en la fase de garabatear en papel, y por lo tanto meditaré profundamente en su respuesta y la de qstarin :) – Rao

1

Evite enredarse emocional o intelectualmente en lo que es la forma "correcta" de hacer algo. Céntrese en cambio en haciendo. No agregue demasiado valor al código que ya ha escrito, ya que puede que sea necesario cambiar alguno o todos para que sea compatible con lo que desea hacer.

IMO hay mucha energía que se gasta en patrones y técnicas geniales y todo ese jazz. Simplemente escriba un código simple para hacer lo que desea hacer.

El nivel "contiene" todo dentro de él. Puedes comenzar allí. El nivel no necesariamente debe conducirlo todo, pero todo está en el nivel.

El jugador puede moverse, pero solo dentro de los límites del nivel. Por lo tanto, el jugador necesita consultar el nivel para ver si una dirección de movimiento es válida.

El nivel no está tomando elementos del jugador, ni es el daño de nivel. Otros objetos en el nivel están haciendo estas cosas. Esos otros objetos deberían estar buscando al jugador, o tal vez contaron acerca de la proximidad del jugador, y luego pueden hacer lo que quieren directamente al jugador.

Está bien que el nivel sea "propio" del jugador y que el jugador tenga una referencia a su nivel. Esto "tiene sentido" desde una perspectiva OO; te paras en el planeta Tierra y puedes afectarlo, pero te está arrastrando por el universo mientras cavas agujeros.

Hacer cosas simples. Cada vez que algo se complica, averigua cómo hacerlo simple. El código simple es más fácil de trabajar y es más resistente a los errores.

+0

Lo tendré en cuenta. – Rao

Cuestiones relacionadas