Сильно связанные классы, как правило, являются плохой практикой:
- Изменения в одном классе приводят к изменениям в другом.
- Вы не можете протестировать один из классов, не создав (или не насмехаясь) другой. , Что в вашем случае создает циклическую зависимость.
- Оба класса зависят от реализации друг друга, а не от абстракций.
- Труднее другим людям (или вам самим через полгода) понять и рассуждать о первом классе logi c без проверки второго класса.
Поскольку Unity не позволяет нам создавать скрипты, не добавляя их в то, что мне нужно, 2 класса. Я получаю определенные данные (данные А), которые требуют обработки
MVC шаблон для Unity предоставляет полезный трюк для разъединения монобихев:
interface ISomeObjectView {
event Action OnUpdate;
event Action Destroyed;
event Action TriggerEntered;
void SetTransform(Vector3 position, Quaternion rotation);
void AddForce(Vector3 force);
// Other methods or events you need to expose:
// MouseOver, OnFixedUpdate, Move() or SetScale(), ...
}
Сам MonoBehaviour не содержит каких-либо логи c, он просто вызывает события и использует входящие значения:
public void SetTransform(Vector3 position, Quaternion rotation)
{
// Params validation
transform.rotation = rotation;
transform.position = position;
}
private void Update()
=> OnUpdate?.Invoke();
MonoBehaviour logi c необходимо переместить в ваш класс данных или новый класс контроллера. Теперь ваш класс данных просто связывает себя с предоставленными интерфейсными событиями и методами без циклических зависимостей. Monobehaviour не требует никаких ссылок на другие классы, он просто предоставляет методы для манипулирования собой и событиями для захвата ввода.
Этот прием помогает несколькими способами:
- MonoBehaviour не зависит на что-либо и не требует никаких ссылок на другие классы.
- Ваши классы data / logi c не требуют специальных знаний о monobehaviours, только предоставленный интерфейс.
- Вы можете иметь несколько реализаций интерфейса, переключение различных представлений в зависимости от ситуации.
- Легко тестировать, легко насмехаться.
- Вы можете перемещать все «вещи Unity» внутри MonoBehaviour и писать все связанные классы на чистом C#. Если вы хотите.
Обратите внимание, что использование event Action
является не обычным способом для обработки событий! Я думаю, что это очень удобно, но я бы предложил использовать обычный EventHandler
(UnityEvents - еще один вариант, который может удовлетворить ваши потребности).
Обновление : пример простого MVC.
Рассмотрим следующий Player
контроллер:
[Serializable]
public class PlayerInfo {
// Values configurable via inspector
[SerializeField] private float speed = 1.5f;
[SerializeField] private float jumpHeight = 5;
[SerializeField] private float damage = 15;
[SerializeField] private Player playerPrefab;
public float Speed => speed;
public float JumpHeight => jumpHeight ;
public float Damage => damage;
private Player playerInstance;
public void InitializePlayer() {
playerInstance = Instantiate(playerPrefab);
playerInstance.Info = this;
}
public void TeleportTo(Vector3 newPosition) {
playerInstance.transform.position = newPosition;
}
}
public class Player : MonoBehaviour {
public PlayerInfo Info { get; set; }
private Rigidbody rb;
private void Awake() {
rb = GetComponent<Rigidbody>();
}
private void Update() {
if (Input.GetButtonDown("Jump")
rb.AddForce(Vector3.up * info.JumpHeight);
Vector3 movement = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
rb.AddForce(movement * info.Speed);
}
private void OnTriggerEnter(Collider other) {
var enemy = other.GetComponent<Enemy>();
if (enemy != null)
enemy.TakeHit(info.Damage);
}
}
Вот вам go. PlayerInfo создается до Player. PlayerInfo ссылается на Player и Player ссылается на PlayerInfo. Игрок используется для перемещения объекта игры и нападения на врагов, PlayerInfo содержит необходимую информацию. Что мы можем сделать здесь?
Во-первых, переписать MonoBehaviour без каких-либо логи c:
public class PlayerView : MonoBehaviour {
private Rigidbody rb;
// Events for future subscription.
public event Action OnUpdate;
public event TriggerEntered<Collider>;
// Simple initialization of required components.
private void Awake() {
rb = GetComponent<Rigidbody>();
}
// Unity methods doing nothing but invoking events.
private void Update() {
OnUpdate?.Invoke();
}
private void OnTriggerEnter(Collider other) {
TriggerEntered?.Invoke(other);
}
// We still need a method to move our player, right?
public void Move(Vector3 direction) {
rb.AddForce(direction);
}
public void SetPosition(Vector3 position) {
transform.position = position;
}
}
Теперь вам нужен класс, содержащий данные об игроке:
[Serializable]
public class PlayerModel {
[SerializeField] private float speed = 1.5f;
[SerializeField] private float jumpHeight = 5;
[SerializeField] private float damage = 15;
public float Speed => speed;
public float JumpHeight => jumpHeight ;
public float Damage => damage;
}
Теперь нам нужен способ объединить эти два элемента вместе:
public class PlayerController {
private readonly PlayerModel model;
private readonly PlayerView view;
public PlayerController(PlayerModel model, PlayerView view) {
// Validate values here.
this.model = model;
this.view = view;
// Linking logic to events.
view.OnUpdate += Move;
view.TriggerEntered += Attack;
}
// Actual logic moved here.
private void Move() {
Vector3 movement = Vector3.zero;
if (Input.GetButtonDown("Jump")
movement += Vector3.up * model.JumpHeight;
movement += new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")) * model.Speed;
view.Move(movement);
}
private void Attack(Collider other) {
var enemy = other.GetComponent<Enemy>();
if (enemy != null)
enemy.TakeHit(model.Damage);
}
// Method from PlayerInfo to set player position without actual movements.
public void MoveTo(Vector3 position) {
view.SetPosition(position);
}
}
Теперь у вас есть 3 класса вместо 2, но классы моделей и представлений очень просты и не требуют каких-либо зависимостей. Вся работа выполняется классом Controller, который получает две другие части и связывает их вместе.
Становится еще лучше, когда интерфейсы вводятся в дополнение к классам: IPlayerModel
, IPlayerView
, IPlayerController
.
Вы можете подумать, что было бы проще создать один класс вместо трех связанных классов, но в конечном итоге вы будете благодарны за использование этого шаблона: представление и модель чрезвычайно просты, легко читаемы, легко проверить на наличие ошибок, легко расширить с помощью новой функциональности. С другой стороны, отдельный класс быстро вырастет до сотен строк и станет кошмаром для тестирования и расширения.
Я настоятельно рекомендую эту статью с более сложными примерами MVC.