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?
architecture
usuario441521
fuente
fuente
Respuestas:
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.
fuente
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 unclass Inventory
. Este es un miembro paraclass Player
, pero internamenteclass Inventory
puede envolver una gran cantidad de código.Otro ejemplo: un personaje jugador puede tener relaciones con los NPC, por lo que puede hacer
class Relation
referencia tanto alPlayer
objeto como alNPC
objeto, pero no pertenecer a ninguno.fuente
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
.IState
tiene en nuestro caso tiene 4 métodos solo por un ejemplo.Loot() Run() Walk() Attack()
Además, tenemos
class InputController
donde verificamos cada entrada del usuario.Ahora al ejemplo real: en
InputController
verificamos si el jugador presiona cualquiera de losWASD or arrows
y luego si también presiona elShift
. Si se presiona solamenteWASD
entonces llamar_currentPlayerState.Walk();
cuando esta happends y tenemoscurrentPlayerState
que ser iguales aWalkState
continuación, en laWalkState.Walk()
que están todos los componentes necesarios para este estado - en este casoMovementSystem
, por lo que hacer el movimiento del jugadorpublic 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
+Shift
presionado? Pero nuestro estado anterior eraWalkState
. En este casoRun()
se llamaráInputController
(no mezcle esto,Run()
se llama porque tenemosWASD
+Shift
check inInputController
no por elWalkState
). Cuando llamamos_currentPlayerState.Run();
enWalkState
- sabemos que tenemos que cambiar_currentPlayerState
aRunState
y lo hacemos en elRun()
deWalkState
y 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é
LootState
cuando 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 elE
botón, llamamos,_currentPlayerState.Loot();
cambiamosLootState
y 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 presionaWASD
? -_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 aIDLEState
.Además, podría hacer otra secuencia de comandos llamada
class BaseState : IState
que tenga implementados todos estos métodos de comportamiento predeterminados, pero los tengavirtual
para que puedaoverride
usarlos enclass LootState : BaseState
tipos 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 comoPutItemToInventory()
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()
, peroBuilding
se encargaría del resto, y cuando el usuario decide construir un edificio específico, le pasa toda la información necesaria al jugadorBuild(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
.IDamagable
por supuesto que tieneTakeDamage()
.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.
fuente
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
Player
clase 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 '
Entity
s' 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
Controller
clases que se utilizan para controlarEntity
s. 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
Controller
enInputController
yAIController
, le permite al jugador controlar efectivamente a cualquieraEntity
en la sala. Este enfoque también ayuda con el modo multijugador al tener unaRemoteController
oNetworkController
clase 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
Controller
si no tienes cuidado. La forma de evitar eso es tenerController
s que están compuestos de otrosController
s, o hacer que laController
funcionalidad dependa de varias propiedades delController
. Por ejemplo, elAIController
tendría unDecisionTree
adjunto, yPlayerCharacterController
podría estar compuesto por varios otrosController
s como aMovementController
, aJumpController
(que contiene una máquina de estados con los estados OnGround, Ascending y Descending), anInventoryUIController
. Un beneficio adicional de esto es queController
se 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.fuente