А сейчас добавим возможность пострелять. Для этого создайте на сцене некоторое количество объектов с коллайдерами, которые будут являться препятствием для пули.
Так же нужно создать системы и добавить их в список в EcsStartup классе.
.Add(new PlayerInputShootSystem())
.Add(new ProjectileCreateSystem())
.Add(new ProjectileMoveSystem())
.Add(new ProjectileDestroyEventSystem())
Система
PlayerInputShootSystem будет создавать
Entity с компонентом
PlayerShootEvent и некоторым данными для создания пули.
Код очень похож на предыдущую систему где создавался компонент с событие движения игрока.
Конечно системы можно унифицировать дабы они двигали сразу все
Entity допустим с компонентом
Move, но в этом случае мы потеряем гибкость и при отключении этой системы перестанут двигаться и игрок и пули.
internal class PlayerInputShootSystem : IEcsRunSystem {
// Тут для пример вместе с EcsFilter<T> есть еще и .Exclude<T> специально
// что бы пули не могли создаваться если игрок движется или
// вращается (если на Entity игрока есть компонент PlayerInputEvent)
private readonly EcsFilter<PlayerRef>.Exclude<PlayerInputEvent> filter = default;
private readonly EcsWorld ecsWorld = default;
private readonly Config config = default;
private readonly GameData gameData = default;
public void Run() {
if (filter.IsEmpty())
return;
if (!Input.GetKeyDown(KeyCode.Space))
return;
foreach (var i in filter) {
ref var playerRef = ref filter.Get1(i);
var tr = playerRef.View.transform;
ecsWorld.NewEntity().Get<PlayerShootEvent>() = new PlayerShootEvent() {
Position = tr.position,
Angle = tr.rotation.eulerAngles.y,
Speed = config.ProjectileStartSpeed
};
// Как помним GameData класс был у нас для временных реалтайм-данных.
// В данном случае при выстреле увеличиваем переменную и
// для примера можно где-то её отрисовать в UI или можно
// посмотреть её значение в редакторе так как у нас
// этот класс сериализован
gameData.shootCount++;
}
}
}
// Данный компонент хоть и заканчивает на "Event", но добавлять
// его в .OneFrame<T> не будем потому-что он создается вместе
// с отдельным Entity. В этом случае лучше удалить руками
// сам Entity, а вместе с ним удалится и этот компонент
internal struct PlayerShootEvent {
public Vector3 Position;
public float Angle;
public float Speed;
}
В системе
ProjectileCreateSystem создаем префаб и
Entity пули. Так же обязательно удаляем
Entity с событием
PlayerShootEvent, которое сигнализирует о необходимости создания пули. Иначе пули будут создаваться в каждом цикле * на количество
Entity в фильтре.
internal class ProjectileCreateSystem : IEcsRunSystem {
// Получаем все Entity с PlayerShootEvent
private readonly EcsFilter<PlayerShootEvent> filter = default;
private readonly EcsWorld ecsWorld = default;
private readonly Config config = default;
public void Run() {
foreach (var i in filter) {
ref var eventData = ref filter.Get1(i);
var position = eventData.Position;
position.y = 0.5f;
var rotation = Quaternion.Euler(0, eventData.Angle, 0);
// Так же создаем и связываем игровой объект и Entity.
var entity = ecsWorld.NewEntity();
ref var projectileRef = ref entity.Get<ProjectileRef>();
// На Entity пули добавляем постоянный компонент,
// который будет хранить постоянную скорость пули
entity.Get<ProjectileMove>().Value = eventData.Speed;
var view = UnityEngine.Object.Instantiate(config.ProjectilePrefab, position, rotation);
view.Entity = entity;
projectileRef.View = view;
// И обязательно удаляем текущий Entity, который содержит
// компонент-событие для создания пули
filter.GetEntity(i).Destroy();
}
}
}
// ref - компонент для Entity пуль
internal struct ProjectileRef {
public ProjectileView View;
}
// View - компонент для пули. Так же тут показан пример как из MonoBehaviour
// взаимодействовать с Entity игрового объекта
public class ProjectileView : MonoBehaviour {
public EcsEntity Entity;
// Если пуля столкнулась с чем-то кроме игрока (PlayerView),
// то добавляем на Entity пули компонент-событие ProjectileDestroyEvent
// благодаря которому эта пуля будет удалена в последующей системе
private void OnTriggerEnter(Collider other) {
if (other.GetComponent<PlayerView>())
return;
// Просто пример как использовать это.
// В данном случае мы получаем ссылку на реалтайм-данные и увеличиваем
// значение переменной, которая будет показывать количество
// столкновений пуль за игру
// Можно так же получать ссылку на мир EcsWorld и создавать Entity
Service<GameData>.Get().collisionsCount++;
// Для примера добавлен параметр в который записывается
// игровой объект с которым произошло столкновение
Entity.Get<ProjectileDestroyEvent>() = new ProjectileDestroyEvent() {
Value = other.gameObject
};
}
}
В данной небольшой системе мы просто двигаем все пули с постоянной скоростью, которая сохранена в компоненте
ProjectileMove. Опять же это сохранение скорости пули только как пример. Данные скорости пули можно так же брать из
Config, как и в системе с движением игрока.
internal class ProjectileMoveSystem : IEcsRunSystem {
private readonly EcsFilter<ProjectileRef, ProjectileMove> filter = default;
public void Run() {
foreach (var i in filter) {
// Помним что "ref" нужен обязательно при получении компонентов,
// которые являются "struct"
ref var projectileRef = ref filter.Get1(i);
ref var projectileMove = ref filter.Get2(i);
// Непосредственно двигаем игровой объект на сцене
var step = projectileMove.Value * Time.deltaTime;
var transform = projectileRef.View.transform;
transform.position += transform.forward * step;
}
}
}
internal struct ProjectileMove {
public float Value;
}
А в этой последней системе
ProjectileDestroyEventSystem просто удаляем и игровой объект и
Entity. Сам
Entity не удалится с "мира", а только пометится как удаленный и в любой момент будет использован заново.
internal class ProjectileDestroyEventSystem : IEcsRunSystem {
private readonly EcsFilter<ProjectileRef, ProjectileDestroyEvent> filter = default;
public void Run() {
foreach (var i in filter) {
ref var projectileRef = ref filter.Get1(i);
ref var destroyData = ref filter.Get2(i);
// Тут просто для примера выводим к консоль название
// игрового объекта с которым столкнулась пуля
Debug.Log(destroyData.Value);
// Удаление gameObject пули на сцене и Entity из "мира"
Object.Destroy(projectileRef.View.gameObject);
filter.GetEntity(i).Destroy();
}
}
}
internal struct ProjectileDestroyEvent {
public GameObject Value;
}
Всё. Выглядит это конечно более сложнее, чем классический код и синхронизация с игровыми объектами на сцене тоже такое себе. Но за то мы получаем максимально разграниченный функционал, который не зависит друг от друга. Так же это всё легко тестится и расширяется хоть до уровня ММО-игры.