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 los WASD or arrows
y luego si también presiona el Shift
. Si se presiona solamente WASD
entonces llamar _currentPlayerState.Walk();
cuando esta happends y tenemos currentPlayerState
que ser iguales a WalkState
continuació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
+ Shift
presionado? Pero nuestro estado anterior era WalkState
. En este caso Run()
se llamará InputController
(no mezcle esto, Run()
se llama porque tenemos WASD
+ Shift
check in InputController
no por el WalkState
). Cuando llamamos _currentPlayerState.Run();
en WalkState
- sabemos que tenemos que cambiar _currentPlayerState
a RunState
y lo hacemos en el Run()
de WalkState
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 el E
botón, llamamos, _currentPlayerState.Loot();
cambiamos LootState
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 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 : IState
que tenga implementados todos estos métodos de comportamiento predeterminados, pero los tenga virtual
para que pueda override
usarlos en class 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 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 Building
se 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
. IDamagable
por 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.