Как в typescript создать массив с разными классами
Хотелось бы иметь возможность хранить некие объекты с разными своими дополнениями в одном списке юнитов на карте. Там могут быть разные животные унаследованные от какого-то общего класса Animal
(да и кто-то еще).
В юнити я бы это сделал простым списком List-Gameobject
, и потом проверял компоненты
Dog dog = itemObj.GetComponent<Dog>();
if (dog != null) dog.woof("Отдай колбасу")
Я не понимаю как сделать подобный подход в ts
. Максимум могу создать массив any[]
, но потом я не могу обращаться к его элементам как к понятным сущностям, а только как к any.
Ответы (2 шт):
Как в typescript создать массив с разными классами
Такое можно сделать, например, вот так...
class Animal {
name: string
constructor(name: string){
this.name = name
}
}
class Dog extends Animal {
//
}
class Cat extends Animal {
//
}
type Test = Dog | Cat
const arr: Test[] = [
new Dog('Шарик'),
new Cat('Мурзик')
]
arr.forEach(o => {
if (o instanceof Dog) console.log('Это собака, имя ', o.name)
})
Так же, как и в unity придётся использовать явное приведение типов.
Вы можете реализовать систему компонентов как в юнити, но для маленьких проектов всё таки реализовать разную логику через ООП.
Реализация разных сущностей через ООП
Первым делом определим сам мир (который хранит все сущности) и саму основу сущности:
type vec2 = [number, number]
class World {
// Одна и таже сущность всё ещё является одной и той же сущностью
// И не может храниться в списке дважды или более раз
// Поэтому используем Set
// (+ оптимизация при удалении и поиске)
entities: Set<Entity> = new Set();
// Заспавнить сущность
spawn(entity: Entity) {
this.entities.add(entity);
entity.world = this;
entity.onSpawn();
return entity;
}
// Уничтожить сущность
destroy(entity: Entity) {
entity.onDestroy();
entity.world = undefined;
this.entities.delete(entity);
}
// Обновить все сущности
update() {
this.entities.forEach((entity) => {
entity.onUpdate();
});
}
}
abstract class Entity {
// Мир, в котором находится сущность
world?: World;
position: vec2 = [0, 0];
// Когда сущность спавниться (можно override)
onSpawn() {}
// Когда сущность уничтожается (можно override)
onUpdate() {}
// Уничтожить сущность
destroy() {
if (this.world) {
this.world.destroy(this);
}
}
// Когда сущность уничтожается (можно override)
onDestroy() {}
// Жива ли сущность
isAlive() {
return this.world !== undefined;
}
}d?: World;
position: vec2 = [0, 0];
start() {}
update() {}
destroy() {
if (this.world) {
this.world.destroy(this);
}
}
}
Наследовав от класса Entity
мы теперь можем создавать новые типы сущностей. Например: создадим подтип сущности Animal
, который будет представлять животных, что издают звуки на каждом onUpdate
:
abstract class Animal extends Entity {
abstract says: string;
override onUpdate() {
super.onUpdate();
console.log(this.says);
}
}
Попрошу обратить внимания, что мы реализовали разную логику в двух разных классах, но до сих пор ещё не разу не проверяли на тип самой сущности. Эта тема затронута более подробно чуть ниже
Ну и теперь для примера создадим 2 простых типа сущности Animal
:
class Cow extends Animal {
says: string = "mooo";
}
class Dog extends Animal {
says: string = "woof"
breed: string; // Порода собаки
constructor(breed: string = "default") {
super();
this.breed = breed;
}
}
Вот пример того, как это можно использовать:
let world = new World();
let cow1 = world.spawn(new Cow());
let cow2 = world.spawn(new Cow());
let dog1 = world.spawn(new Dog("служебная"))
world.update();
// console:
// mooo
// mooo
// woof
Допустим мы имплементировали новый метод для Entity
: onCollide(...)
, который вызывается, когда this
сущность сталкивается с другой сущностью.
Мы хотим, что бы любой экземпляр Cow
реагировал на собак, но только определённой (охотничьей) породы.
Но увы: наша функция onCollide(...)
даёт нам Entity
. Что бы проверить, является ли данная сущность собакой и узнать её породу можно воспользоваться оператором instanceof
, который позволяет проверить, является ли объект экземпляром указанного класса:
class Cow extends Animal {
says: string = "mooo";
onCollide(other: Entity) {
if (other instanceof Dog // Так как мы уже проверили наш Entity на необходимый тип
&& other.breed === "охотничая") // Typescript уже знает, что тут other является экземпляром Dog
{
console.log("*runs away*")
}
}
}
Однако как я уже говорил - если есть возможность, то лучше подобные проверки минимизировать.
В данном случае лучше оставить так, однако если необходимое поведение можно реализовать через ООП, то лучше так и сделать. Пример:
Вместо
class Updatable extends Entity {
update() {...}
}
...
if (entity instanceof Updatable) {
entity.update(); // существует только в типе Updatable, нужна проверка
}
Лучше сделать
class Entity {
update() {}
}
class SomeOtherClass extends Entity {
override update() {...}
}
...
entity.update(); // работает на любом entity