¿Cómo puedo evitar las clases de jugadores gigantes?

46

Casi siempre hay una clase de jugador en un juego. El jugador generalmente puede hacer mucho en el juego, lo que significa que para mí esta clase termina siendo enorme con un montón de variables para soportar cada pieza de funcionalidad que el jugador puede hacer. Cada pieza es bastante pequeña por sí sola, pero combinada termino con miles de líneas de código y se vuelve difícil encontrar lo que necesita y da miedo hacer cambios. Con algo que es básicamente un control general para todo el juego, ¿cómo evitas este problema?

usuario441521
fuente
26
Múltiples archivos o un archivo, el código tiene que ir a alguna parte. Los juegos son complejos. Para encontrar lo que necesita, escriba buenos nombres de métodos y comentarios descriptivos. No tengas miedo de hacer cambios, solo prueba. Y haga una copia de seguridad de su trabajo :)
Chris McFarland
77
Entiendo que tiene que ir a algún lado, pero el diseño del código importa en flexibilidad y mantenimiento. Tener una clase o grupo de código que son miles de líneas simplemente no me parece tampoco.
user441521
17
@ChrisMcFarland no sugiere hacer una copia de seguridad, sugiere el código de versión XD.
GameDeveloper
1
@ChrisMcFarland Estoy de acuerdo con GameDeveloper. Tener un control de versiones como Git, svn, TFS, ... hace que el desarrollo sea mucho más fácil debido a que puede deshacer grandes cambios mucho más fácilmente y poder recuperarse fácilmente de cosas como eliminar accidentalmente su proyecto, falla de hardware o corrupción de archivos.
Nzall
3
@ TylerH: Estoy totalmente en desacuerdo. Las copias de seguridad no permiten fusionar muchos cambios exploratorios, ni atan metadatos útiles a conjuntos de cambios, ni permiten flujos de trabajo sanos de múltiples desarrolladores. Puede usar el control de versiones como un sistema de respaldo de punto en el tiempo muy poderoso, pero le falta mucho del potencial de esas herramientas.
Phoshi

Respuestas:

67

Usualmente usaría un sistema de componentes de entidad (Un sistema de componentes de entidad es una arquitectura basada en componentes). Esto también facilita la creación de otras entidades, y también puede hacer que los enemigos / NPC tengan los mismos componentes que el jugador.

Este enfoque va en la dirección exactamente opuesta a un enfoque orientado a objetos. Todo en el juego es una entidad. La entidad es solo un caso sin ninguna mecánica de juego incorporada. Tiene una lista de componentes y una forma de manipularlos.

Por ejemplo, el jugador tiene un componente de posición, un componente de animación y un componente de entrada y cuando el usuario presiona el espacio, desea que el jugador salte.

Puede lograr esto dando a la entidad del jugador un componente de salto, que cuando se llama hace que el componente de animación cambie a la animación de salto y hace que el jugador tenga una velocidad y positiva en el componente de posición. En el componente de entrada escucha la tecla de espacio y llama al componente de salto. (Esto es solo un ejemplo, debe tener un componente controlador para el movimiento).

Esto ayuda a dividir el código en módulos más pequeños y reutilizables, y puede dar como resultado un proyecto más organizado.

Bálint
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
MichaelHouse
8
Si bien entiendo los comentarios en movimiento que deben moverse, no mueva los que desafían la precisión de la respuesta. Eso debería ser obvio, ¿no?
bug-a-lot
20

Los juegos no son únicos en esto; Las clases de Dios son un antipatrón en todas partes.

Una solución común es dividir la clase grande en un árbol de clases más pequeñas. Si el jugador tiene un inventario, no formes parte de la gestión de inventario class Player. En cambio, cree un class Inventory. Este es un miembro para class Player, pero internamente class Inventorypuede envolver una gran cantidad de código.

Otro ejemplo: un personaje jugador puede tener relaciones con los NPC, por lo que puede hacer class Relationreferencia tanto al Playerobjeto como al NPCobjeto, pero no pertenecer a ninguno.

MSalters
fuente
Sí, solo estaba buscando ideas sobre cómo hacer esto. Cuál era la mentalidad porque hay muchas funciones de pequeñas piezas, por lo que, aunque la codificación no es natural, para mí de todos modos, romper esas pequeñas piezas de funcionalidad. Sin embargo, resulta obvio que todas esas pequeñas piezas de funcionalidad comienzan a hacer que la clase de jugador sea enorme.
user441521
1
La gente suele decir que algo es una clase de dios o un objeto de dios, cuando contiene y gestiona cualquier otra clase / objeto en el juego.
Bálint
11

1) Reproductor: arquitectura basada en máquina de estado + componente.

Componentes habituales para Player: HealthSystem, MovementSystem, InventorySystem, ActionSystem. Esas son todas las clases como class HealthSystem.

No recomiendo usarlo Update()allí (no tiene sentido en los casos habituales tener una actualización en el sistema de salud a menos que lo necesite para algunas acciones allí cada cuadro, esto rara vez ocurre. Un caso en el que también puede pensar: el jugador se envenena y lo necesita) para perder salud de vez en cuando, aquí sugiero usar corutinas. Otra regenera constantemente la salud o el poder de ejecución, simplemente tomas la salud o el poder actual y llamas a la rutina para que llegue a ese nivel cuando llegue el momento. Rompe la rutina cuando la salud esté llena o estaba dañado o comenzó a correr de nuevo y así sucesivamente. OK, eso fue un poco extraño, pero espero que haya sido útil) .

Estados: LootState, RunState, WalkState, AttackState, IDLEState.

Cada estado hereda de interface IState. IStatetiene en nuestro caso tiene 4 métodos solo por un ejemplo.Loot() Run() Walk() Attack()

Además, tenemos class InputControllerdonde verificamos cada entrada del usuario.

Ahora al ejemplo real: en InputControllerverificamos si el jugador presiona cualquiera de los WASD or arrowsy luego si también presiona el Shift. Si se presiona solamente WASDentonces llamar _currentPlayerState.Walk();cuando esta happends y tenemos currentPlayerStateque ser iguales a WalkStatecontinuación, en la WalkState.Walk() que están todos los componentes necesarios para este estado - en este caso MovementSystem, por lo que hacer el movimiento del jugador public void Walk() { _playerMovementSystem.Walk(); }- ver lo que tenemos aquí? Tenemos una segunda capa de comportamiento y eso es muy bueno para el mantenimiento y la depuración de código.

Ahora al segundo caso: ¿qué pasa si tenemos WASD+ Shiftpresionado? Pero nuestro estado anterior era WalkState. En este caso Run()se llamará InputController(no mezcle esto, Run()se llama porque tenemos WASD+ Shiftcheck in InputControllerno por el WalkState). Cuando llamamos _currentPlayerState.Run();en WalkState- sabemos que tenemos que cambiar _currentPlayerStatea RunStatey lo hacemos en el Run()de WalkStatey llamar de nuevo dentro de este método, pero ahora con un estado diferente, ya que no queremos perder a la acción de este marco. Y ahora, por supuesto, llamamos _playerMovementSystem.Run();.

Pero, ¿para qué LootStatecuando el jugador no puede caminar o correr hasta que suelta el botón? Bueno, en este caso, cuando comenzamos a saquear, por ejemplo, cuando presionamos el Ebotón, llamamos, _currentPlayerState.Loot();cambiamos LootStatey ahora llamamos a su llamada desde allí. Allí, por ejemplo, llamamos al método de colisión para obtener si hay algo para saquear dentro del rango. Y llamamos a la rutina donde tenemos una animación o donde la iniciamos y también verificamos si el jugador todavía mantiene presionado el botón, si no se rompe la rutina, en caso afirmativo le damos un botín al final de la rutina. Pero, ¿y si el jugador presiona WASD? - _currentPlayerState.Walk();se llama, pero aquí está lo bonito de la máquina de estado, enLootState.Walk()tenemos un método vacío que no hace nada o como yo haría como característica: los jugadores dicen: "Oye, aún no he saqueado esto, ¿puedes esperar?". Cuando termina de saquear, cambiamos a IDLEState.

Además, podría hacer otra secuencia de comandos llamada class BaseState : IStateque tenga implementados todos estos métodos de comportamiento predeterminados, pero los tenga virtualpara que pueda overrideusarlos en class LootState : BaseStatetipos de clases.


El sistema basado en componentes es excelente, lo único que me molesta son las instancias, muchas de ellas. Y se necesita más memoria y trabajo para el recolector de basura. Por ejemplo, si tienes 1000 instancias de enemigo. Todos ellos tienen 4 componentes. 4000 objetos en lugar de 1000. Mb no es gran cosa (no he ejecutado pruebas de rendimiento) si consideramos todos los componentes que tiene unitobject.


2) Arquitectura basada en herencia. Aunque notará que no podemos deshacernos completamente de los componentes, en realidad es imposible si queremos tener un código limpio y funcional. Además, si queremos usar Patrones de diseño que se recomienda usar en casos apropiados (no los uses demasiado, se llama sobregeneración).

Imagina que tenemos una clase de jugador que tiene todas las propiedades que necesita para salir de un juego. Tiene salud, maná o energía, puede moverse, correr y usar habilidades, tiene un inventario, puede fabricar objetos, saquear objetos, incluso puede construir algunas barricadas o torretas.

En primer lugar, voy a decir que el inventario, la fabricación, el movimiento, la construcción deben basarse en componentes porque no es responsabilidad del jugador tener métodos como AddItemToInventoryArray(), aunque el jugador puede tener un método como PutItemToInventory()ese que se llamará método descrito anteriormente (2 capas - podemos agregue algunas condiciones dependiendo de las diferentes capas).

Otro ejemplo con la construcción. El jugador puede llamar a algo así OpenBuildingWindow(), pero Buildingse encargaría del resto, y cuando el usuario decide construir un edificio específico, le pasa toda la información necesaria al jugador Build(BuildingInfo someBuildingInfo)y el jugador comienza a construirlo con todas las animaciones necesarias.

SÓLIDO - Principios de OOP. S - responsabilidad única: eso que hemos visto en ejemplos anteriores. Sí, pero ¿dónde está la herencia?

Aquí: ¿debería la salud y otras características del jugador ser manejadas por otra entidad? Yo creo que no. No puede haber un jugador sin salud, si hay uno, simplemente no heredamos. Por ejemplo, tenemos IDamagable, LivingEntity, IGameActor, GameActor. IDamagablepor supuesto que tiene TakeDamage().

class LivinEntity : IDamagable {

   private float _health; // For fields that are the same between Instances I would use Flyweight Pattern.

   public void TakeDamage() {
       ....
   }
}

class GameActor : LivingEntity, IGameActor {
    // Here goes state machine and other attached components needed.
}

class Player : GameActor {
   // Inventory, Building, Crafting.... components.
}

Entonces, aquí no podría dividir los componentes de la herencia, pero podemos mezclarlos como ve. También podemos hacer algunas clases base para el sistema de construcción, por ejemplo, si tenemos diferentes tipos de este y no queremos escribir más código del necesario. De hecho, también podemos tener diferentes tipos de edificios y, de hecho, ¡no hay una buena manera de hacerlo basado en componentes!

OrganicBuilding : Building, TechBuilding : Building. No necesita crear 2 componentes y escribir código allí dos veces para operaciones comunes o propiedades de construcción. Y luego agregarlos de manera diferente, puede usar el poder de la herencia y luego el polimorfismo y la encapsulación.


Sugeriría usar algo intermedio. Y no usar en exceso los componentes.


Recomiendo leer este libro sobre Patrones de programación de juegos , es gratis en la WEB.

Candid Moon _Max_
fuente
Voy a cavar más tarde esta noche, pero para tu información, no estoy usando la unidad, así que tendré que ajustar algo, lo cual está bien.
user441521
Oh, sry, pensé que aquí había una etiqueta de Unity, mi mal. Lo único es MonoBehavior: es solo una clase base para cada instancia en la escena en el editor de Unity. En cuanto a Physics.OverlapSphere (): es un método que crea un colisionador de esferas durante el marco y comprueba lo que toca. Las rutinas son como una actualización falsa, sus llamadas se pueden reducir a cantidades más pequeñas que los fps en la PC de los jugadores, lo que es bueno para el rendimiento. Start (): solo un método llamado una vez cuando se crea la instancia. Todo lo demás debe aplicarse en cualquier otro lugar. La siguiente parte no usaré nada con Unity. Sry Espero que esto haya aclarado algo.
Candid Moon _Max_
He usado Unity antes, así que entiendo la idea. Estoy usando Lua, que también tiene corutinas, por lo que las cosas deberían traducirse bastante bien.
user441521
Esta respuesta parece demasiado específica para Unity considerando la falta de la etiqueta de Unity. Si lo hiciera más genérico e hiciera que la unidad fuera más un ejemplo, esta sería una respuesta mucho mejor.
Pharap
@CandidMoon Sí, eso está mejor.
Pharap
4

Este problema no tiene una solución mágica, pero existen varios enfoques diferentes, casi todos los cuales giran en torno al principio de "separación de preocupaciones". Otras respuestas ya han discutido el popular enfoque basado en componentes, pero hay otros enfoques que se pueden usar en lugar de o junto con la solución basada en componentes. Voy a discutir el enfoque de entidad-controlador, ya que es una de mis soluciones preferidas para este problema.

En primer lugar, la idea misma de una Playerclase es engañosa en primer lugar. Muchas personas tienden a pensar en un personaje jugador, personajes npc y monstruos / enemigos como clases diferentes, cuando en realidad todos tienen mucho en común: todos están dibujados en la pantalla, todos se mueven, podrían todos tienen inventarios, etc.

Esta forma de pensar conduce a un enfoque en el que los personajes jugadores, los personajes no jugadores y los monstruos / enemigos son tratados como ' Entitys' en lugar de ser tratados de manera diferente. Naturalmente, sin embargo, tienen que comportarse de manera diferente: el personaje del jugador debe controlarse a través de la entrada y npcs necesita ai.

La solución a esto es tener Controllerclases que se utilizan para controlar Entitys. Al hacer esto, toda la lógica pesada termina en el controlador y todos los datos y elementos comunes se almacenan en la entidad.

Además, al subclasificar Controlleren InputControllery AIController, le permite al jugador controlar efectivamente a cualquiera Entityen la sala. Este enfoque también ayuda con el modo multijugador al tener una RemoteControllero NetworkControllerclase que opera a través de comandos de una transmisión de red.

Esto puede resultar en que gran parte de la lógica se calce en uno Controllersi no tienes cuidado. La forma de evitar eso es tener Controllers que están compuestos de otros Controllers, o hacer que la Controllerfuncionalidad dependa de varias propiedades del Controller. Por ejemplo, el AIControllertendría un DecisionTreeadjunto, y PlayerCharacterControllerpodría estar compuesto por varios otros Controllers como a MovementController, a JumpController(que contiene una máquina de estados con los estados OnGround, Ascending y Descending), an InventoryUIController. Un beneficio adicional de esto es que Controllerse pueden agregar nuevos s a medida que se agregan nuevas características: si un juego comienza sin un sistema de inventario y se agrega uno, se puede agregar un controlador para él más adelante.

Pharap
fuente
Me gusta la idea de esto, pero parece haber transferido todo el código a la clase de controlador, dejándome con el mismo problema.
user441521
@ user441521 Me acabo de dar cuenta de que había un párrafo adicional que iba a agregar, pero lo perdí cuando mi navegador falló. Lo agregaré ahora. Básicamente, puede tener diferentes controladores que pueden componerlos en controladores agregados para que cada controlador maneje cosas diferentes. por ejemplo, AggregateController.Controllers = {JumpController (keybinds), MoveController (keybinds), InventoryUIController (keybinds, uisystem)}
Pharap