Предыдущая часть
В прошлой части я показал уже всем знакомые приемы использования тэгов для взаимодействия между объектами.
А в этой части я расскажу какие приемы, разработчики знакомые с ООП, используют чтобы упростить задачу взаимодействия объектов друг с другом.
Давайте представим три машины, как в случае трех объектов взаимодействия из прошлой части.
Все три машины имеют разный дизайн, может даже имеют разное кол во колес или дверей, в общем внешне машины очень не похожи друг на друга.
Но у всех есть кое что что их всех объединяет - это двигатель. Когда мы видим машину мы сразу понимаем что у нее есть двигатель и не важно какая это машина, какой у нее дизайн и т д. Мы всегда знаем чтобы взаимодействовать с двигателем машины не нужно знать ее название, или марку, или сколько у нее колес, мы можем на прямую сразу работать с двигателем, т к это объединяющий элемент всех машин. Возвращаясь к примеру трех объектов взаимодействия - они все разные, обладают разными свойствами, у них разные имена управляющих скриптов, разные тэги и тд, но у них есть кое что что их объединяет - с каждым из них может взаимодействовать игрок.
Сразу скажу что у каждого имеется свой опыт подхода к ООП, я лишь расскажу о том что вижу чаще всего.
Когда программист замечает сходные свойства двух или более объектов, ему сразу на ум приходит решение объединить эти объекты, каким то более базовым объектом, от которого уже дальше будут наследоваться остальные объекты - один из базовых принципов ООП.
Так вот и в нашем случае - с каждым из объектов можно взаимодействовать, и следовательно эти объекты можно объединить каким-то более базовым объектом с которым можно будет взаимодействовать и от которого будут наследоваться все три объекта.
Для решения этой задачи можно было бы наследовать все три объекта от одного базового класса - к примеру под названием DynamicObject который был бы унаследован от MonoBehaviour.
Синтаксис:
Используется csharp
public class DynamicObject : MonoBehaviour {
}
}
И который бы имел метод под названием HitObject.
Синтаксис:
Используется csharp
public class DynamicObject : MonoBehaviour {
public void HitObject(Vector2 hitDirection, float damage) {
}
}
public void HitObject(Vector2 hitDirection, float damage) {
}
}
Этот метод принимал бы два параметра:
hitDirection - вектор удара
damage - значение нанесенного урона
А дальше мы бы просто унаследовали все три объекта взаимодействия(Стену, ящик и врага) от этого класса.
Синтаксис:
Используется csharp
public class Enemy : DynamicObject {
public float health;
}
public class Box : DynamicObject {
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
}
public class Wall : DynamicObject {
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public enum WallType {
Brick,
Metal,
Glass
}
}
public float health;
}
public class Box : DynamicObject {
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
}
public class Wall : DynamicObject {
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public enum WallType {
Brick,
Metal,
Glass
}
}
Теперь все три объекта, наследуясь от класса DynamicObject, будут иметь один общий метод HitObject.
Но здесь возникает сложность, метод HitObject находится в классе DynamicObject, а не в классах объектов что наследуются от него, как же тогда игрок будет взаимодействовать именно с этими тремя объектами, а не с их базовым классом!?
В этом случае есть пару выходов из ситуации.
1) Сделать метод HitObject виртуальным, чтобы другие наследующиеся классы могли его(метод) перегрузить и каждый объект по отдельности имел бы свою реализацию метода HitObject. Выглядело бы это примерно так.
Синтаксис:
Используется csharp
public class DynamicObject : MonoBehaviour {
public virtual void HitObject(Vector2 hitDirection, float damage) {
}
}
public virtual void HitObject(Vector2 hitDirection, float damage) {
}
}
И теперь каждый объект взаимодействия может перегрузить данный метод чтобы по своему его реализовать.
Синтаксис:
Используется csharp
public class Enemy : DynamicObject {
public float health;
public override void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Box : DynamicObject {
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
public override void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Wall : DynamicObject {
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public override void HitObject(Vector2 hitDirection, float damage) {
}
public enum WallType {
Brick,
Metal,
Glass
}
}
public float health;
public override void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Box : DynamicObject {
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
public override void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Wall : DynamicObject {
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public override void HitObject(Vector2 hitDirection, float damage) {
}
public enum WallType {
Brick,
Metal,
Glass
}
}
Теперь каждый объект имеет свою реализацию метода HitObject.
Но есть также еще более правильный подход к решению этой проблемы.
2) Объявить базовый класс DynamicObject абстрактным и сделать метод HitObject абстрактным, чтобы каждый объект также мог реализовать его по своему усмотрению, но при этому у базового класса отпала бы необходимость иметь у себя точно такую же реализацию этого же метода.
Выглядело бы это так.
Синтаксис:
Используется csharp
public abstract class DynamicObject : MonoBehaviour {
public abstract void HitObject(Vector2 hitDirection, float damage);
}
public abstract void HitObject(Vector2 hitDirection, float damage);
}
Как вы видите абстрактные методы(и др поля) не имеют конкретной реализации, им как бы все равно что будет происходить и как будет реализован абстрактный метод - главное что он имеет этот метод и все.
В объектах взаимодействия ни каких изменений не последует, т к они также будут перегружать абстрактный метод из базового абстрактного класса.
Но и этот способ немного но далек от истины!
Дело в том что ВЫ как программист должны оценить не только на сколько объекты похожи друг на друга, но также и объем работ связанны с их использованием. В нашем случае игрок просто бьет объекты и больше ни чего с ними не делает, а для того чтобы это произошло нам пришлось создать целый, хоть и не большой, новый класс с одним лишь методом. Поэтому должен возникнуть вопрос: а зачем вообще мне что то откуда то наследовать, если есть решение еще более проще - интерфейсы!? Так как в языке C Sharp нет множественного наследования и где любой класс может наследоваться только от ОДНОГО любого другого класса, появилось другое решение - интерфейсы. Любой класс или структура может наследоваться от сколь угодно интерфейсов. Подробно о них(интерфейсах) можно почитать на мсдн
В нашем случае интерфейс будет выполнять практически ту же работу что и базовый абстрактный класс DynamicObject за исключением лишь того, что нам больше не нужен будет этот класс как объединяющее звено, им будет выступать интерфейс, а также не нужно будет перегружать метод HitObject в каждом объекте взаимодействия.
И так поэтому сначала сотрем класс DynamicObject, т к он нам больше не нужен и создадим простой интерфейс с одним методом HitObject.
Синтаксис:
Используется csharp
public interface IHittable {
void HitObject(Vector2 hitDirection, float damage);
}
void HitObject(Vector2 hitDirection, float damage);
}
Как и в абстрактном классе, методы в интерфейсах не имеют реализации - как и абстрактные методы.
И теперь вернем всем трем объектам базовый класс MonoBehaviour и дополнительно унаследуем его от интерфейса IHittable.
Синтаксис:
Используется csharp
public class Enemy : MonoBehaviour, IHittable {
public float health;
public void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Box : MonoBehaviour, IHittable {
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
public void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Wall : MonoBehaviour, IHittable {
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public void HitObject(Vector2 hitDirection, float damage) {
}
public enum WallType {
Brick,
Metal,
Glass
}
}
public float health;
public void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Box : MonoBehaviour, IHittable {
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
public void HitObject(Vector2 hitDirection, float damage) {
}
}
public class Wall : MonoBehaviour, IHittable {
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public void HitObject(Vector2 hitDirection, float damage) {
}
public enum WallType {
Brick,
Metal,
Glass
}
}
Теперь когда мы проделали эту работу мы получили опять же разные объекты с разными свойствами, но всех их объединяет один интерфейс, как и двигатель объединяет все машины из примера с машинами.
Теперь мы можем написать свою реализацию метода HitObject в каждом объекте взаимодействия, также как это мы делали при использовании тэгов.
Начнем с объекта Enemy. Как вы помните когда игрок бьет врага то у него просто отнимается уровень здоровья, поэтому повторим эти же действия только уже не в методе OnTriggerEnter класса Player, а в методе HitObject класса Enemy.
Синтаксис:
Используется csharp
public class Enemy : MonoBehaviour, IHittable {
public float health;
public void HitObject(Vector2 hitDirection, float damage) {
this.health -= damage;
}
}
public float health;
public void HitObject(Vector2 hitDirection, float damage) {
this.health -= damage;
}
}
Переходим к объекту Box. В этом случае заставляем объект отпрыгнуть от игрока только уже в методе HitObject.
Синтаксис:
Используется csharp
public class Box : MonoBehaviour, IHittable {
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
public void HitObject(Vector2 hitDirection, float damage) {
Vector2 force = hitDirection * damage;
this.body.AddForce(force);
}
}
public Rigidbody body = null;
private void Start() {
this.body = this.GetComponent<Rigidbody>() ?? this.gameObject.AddComponent<Rigidbody>();
}
public void HitObject(Vector2 hitDirection, float damage) {
Vector2 force = hitDirection * damage;
this.body.AddForce(force);
}
}
И закончим объектом Wall.
Синтаксис:
Используется csharp
public class Wall : MonoBehaviour, IHittable {
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public void HitObject(Vector2 hitDirection, float damage) {
if (this.wallType == WallType.Glass) this.prochnost -= damage;
else if (this.wallType == WallType.Brick) this.prochnost -= damage / 2f;
else if (this.wallType == WallType.Metall) this.prochnost -= damage / 3f;
}
public enum WallType {
Brick,
Metal,
Glass
}
}
public WallType wallType = WallType.Glass;
public int prochnost = 100;
public void HitObject(Vector2 hitDirection, float damage) {
if (this.wallType == WallType.Glass) this.prochnost -= damage;
else if (this.wallType == WallType.Brick) this.prochnost -= damage / 2f;
else if (this.wallType == WallType.Metall) this.prochnost -= damage / 3f;
}
public enum WallType {
Brick,
Metal,
Glass
}
}
Готово. Теперь все три объекта готовы к взаимодействию - переходим к методу OnTriggerEnter класса Player и очистим его.
Синтаксис:
Используется csharp
public class Player : MonoBehaviour {
public float damage;
public Vector2 direction;
private void OnTriggerEnter(Collider collider) {
}
}
public float damage;
public Vector2 direction;
private void OnTriggerEnter(Collider collider) {
}
}
Теперь для взаимодействия на объект нужно просто найти нужный интерфейс и вызвать метод HitObject. Вот как это выглядит.
Синтаксис:
Используется csharp
public class Player : MonoBehaviour {
public float damage;
public Vector2 direction;
private void OnTriggerEnter(Collider collider) {
IHittable hitObject = collider.gameObject.GetComponent<MonoBihaviour>() as IHittable;
}
}
public float damage;
public Vector2 direction;
private void OnTriggerEnter(Collider collider) {
IHittable hitObject = collider.gameObject.GetComponent<MonoBihaviour>() as IHittable;
}
}
Как вы видите нельзя напрямую взять компонент объекта как интерфейс, но можно его преобразовать с помощью оператора as.
Скрытый текст:
И когда мы получили ссылку на интерфейс объекта, сначала мы должны точно проверить это, мы можем вызвать метод HitObject объекта взаимодействия.
Синтаксис:
Используется csharp
public class Player : MonoBehaviour {
public float damage;
public Vector2 direction;
private void OnTriggerEnter(Collider collider) {
IHittable hitObject = collider.gameObject.GetComponent<MonoBihaviour>() as IHittable;
if (hitObject != null) hitObject.HitObject(this.direction, this.damage);
}
}
public float damage;
public Vector2 direction;
private void OnTriggerEnter(Collider collider) {
IHittable hitObject = collider.gameObject.GetComponent<MonoBihaviour>() as IHittable;
if (hitObject != null) hitObject.HitObject(this.direction, this.damage);
}
}
И передать в него необходимые параметры.
Ну вот и готово - как и обещал две строки, вместо 17ти, и без использования тэгов.
Этот пример показывает как можно и как даже нужно устанавливать связь между объектами, чаще всего.
Ведь тэги просто заносят объекты в категории без указания, что это за объекты и без какой либо еще дополнительной информации, как допустим вы дали бы слепому человеку в одну руку яблоко, а в другую кокос и задали бы этим объектам тэги "Фрукт", и когда вы скажите слепому человеку что фрукт съедобный, он попробует съесть яблоко и у него это получиться, а вот когда он попробует укусить кокос он поймет что скорей всего больше ни чего не сможет кусать.
Да, тэги просты в использовании и в unity они часто используются в случаях когда теряются любые другие ссылки на объекты, или когда их просто нет(ссылок). Также в отличие от тэгов, с помощью наследования можно искать объекты на сцене через метод FindObjectsOfType, конечно только если выбранный тип прямо или косвенно наследуется от UnityEngine.Object. За тэгами нужно следить, т к это простые строки. Когда здесь или там изменил их или когда написал имя тэга с ошибкой - получается что все - код не будет выполняться.
Вот о чем я хотел рассказать в этой статье. И надеюсь у меня получилось донести смысл чем же отличается взаимодействие с помощью тэгов и прямым доступом к объекту.
Автор: этот хрен.