[Первая часть: тайлы и поиск пути]
GameTileContentType
.public enum GameTileContentType {
Empty, Destination, Wall, SpawnPoint
}
[SerializeField]
GameTileContent spawnPointPrefab = default;
…
public GameTileContent Get (GameTileContentType type) {
switch (type) {
case GameTileContentType.Destination: return Get(destinationPrefab);
case GameTileContentType.Empty: return Get(emptyPrefab);
case GameTileContentType.Wall: return Get(wallPrefab);
case GameTileContentType.SpawnPoint: return Get(spawnPointPrefab);
}
Debug.Assert(false, "Unsupported type: " + type);
return null;
}
GameBoard
. Но точки спауна не влияют на поиск пути, поэтому после изменения нам не нужно искать новые пути. public void ToggleSpawnPoint (GameTile tile) {
if (tile.Content.Type == GameTileContentType.SpawnPoint) {
tile.Content = contentFactory.Get(GameTileContentType.Empty);
}
else if (tile.Content.Type == GameTileContentType.Empty) {
tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint);
}
}
List<GameTile> spawnPoints = new List<GameTile>();
…
public void ToggleSpawnPoint (GameTile tile) {
if (tile.Content.Type == GameTileContentType.SpawnPoint) {
if (spawnPoints.Count > 1) {
spawnPoints.Remove(tile);
tile.Content = contentFactory.Get(GameTileContentType.Empty);
}
}
else if (tile.Content.Type == GameTileContentType.Empty) {
tile.Content = contentFactory.Get(GameTileContentType.SpawnPoint);
spawnPoints.Add(tile);
}
}
Initialize
теперь должен задать точку спауна, чтобы создать исходное правильное состояние поля. Давайте просто включим первый тайл, который находится в левом нижнем углу. public void Initialize (
Vector2Int size, GameTileContentFactory contentFactory
) {
…
ToggleDestination(tiles[tiles.Length / 2]);
ToggleSpawnPoint(tiles[0]);
}
Input.GetKey
) будет переключаться состояние конечной точки void HandleAlternativeTouch () {
GameTile tile = board.GetTile(TouchRay);
if (tile != null) {
if (Input.GetKey(KeyCode.LeftShift)) {
board.ToggleDestination(tile);
}
else {
board.ToggleSpawnPoint(tile);
}
}
}
GetSpawnPoint
с параметром-индексом. public GameTile GetSpawnPoint (int index) {
return spawnPoints[index];
}
public int SpawnPointCount => spawnPoints.Count;
GameObjectFactory
. Нам будет достаточно одного метода CreateGameObjectInstance
с общим параметром префаба, который создаёт и возвращает экземпляр, а также занимается управлением всей сценой. Сделаем метод protected
, то есть он будет доступен только классу и всем типам, которые от него наследуют. Это всё, что делает класс, он не предназначен для использования в качестве полнофункциональной фабрики. Поэтому пометим его как abstract
, что не позволит создавать экземпляры его объектов.using UnityEngine;
using UnityEngine.SceneManagement;
public abstract class GameObjectFactory : ScriptableObject {
Scene scene;
protected T CreateGameObjectInstance<T> (T prefab) where T : MonoBehaviour {
if (!scene.isLoaded) {
if (Application.isEditor) {
scene = SceneManager.GetSceneByName(name);
if (!scene.isLoaded) {
scene = SceneManager.CreateScene(name);
}
}
else {
scene = SceneManager.CreateScene(name);
}
}
T instance = Instantiate(prefab);
SceneManager.MoveGameObjectToScene(instance.gameObject, scene);
return instance;
}
}
GameTileContentFactory
так, чтобы он наследовал этот тип фабрики и использовал CreateGameObjectInstance
в своём методе Get
, а затем уберём из него код управления сценой.using UnityEngine;
[CreateAssetMenu]
public class GameTileContentFactory : GameObjectFactory {
…
//Scene contentScene;
…
GameTileContent Get (GameTileContent prefab) {
GameTileContent instance = CreateGameObjectInstance(prefab);
instance.OriginFactory = this;
//MoveToFactoryScene(instance.gameObject);
return instance;
}
//void MoveToFactoryScene (GameObject o) {
// …
//}
}
EnemyFactory
, который создаёт экземпляр одного префаба Enemy
с помощью метода Get
вместе с сопровождающим методом Reclaim
.using UnityEngine;
[CreateAssetMenu]
public class EnemyFactory : GameObjectFactory {
[SerializeField]
Enemy prefab = default;
public Enemy Get () {
Enemy instance = CreateGameObjectInstance(prefab);
instance.OriginFactory = this;
return instance;
}
public void Reclaim (Enemy enemy) {
Debug.Assert(enemy.OriginFactory == this, "Wrong factory reclaimed!");
Destroy(enemy.gameObject);
}
}
Enemy
изначально должен только отслеживать свою исходную фабрику.using UnityEngine;
public class Enemy : MonoBehaviour {
EnemyFactory originFactory;
public EnemyFactory OriginFactory {
get => originFactory;
set {
Debug.Assert(originFactory == null, "Redefined origin factory!");
originFactory = value;
}
}
}
Enemy
.Game
должен получит ссылку на фабрику врагов. Так как нам нужно много врагов, добавим опцию конфигурации для настройки скорости спаунинга, выражаемую в количестве врагов за секунду. Приемлемым кажется интервал 0.1–10 со значением 1 по умолчанию. [SerializeField]
EnemyFactory enemyFactory = default;
[SerializeField, Range(0.1f, 10f)]
float spawnSpeed = 1f;
Update
, увеличивая его на скорость, умноженную на дельту времени. Если величина prggress превышает 1, то выполняем его декремент и спауним врага с помощью нового метода SpawnEnemy
. Продолжаем это делать, пока progress превышает 1 на случай, если скорость слишком высока и время кадра оказалось очень длинным, чтобы одновременно не создалось несколько врагов. float spawnProgress;
…
void Update () {
…
spawnProgress += spawnSpeed * Time.deltaTime;
while (spawnProgress >= 1f) {
spawnProgress -= 1f;
SpawnEnemy();
}
}
SpawnEnemy
получит случайную точку спауна с поля и создаст в этом тайле врага. Мы дадим Enemy
метод SpawnOn
, чтобы он правильно себя спозицинировал. void SpawnEnemy () {
GameTile spawnPoint =
board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount));
Enemy enemy = enemyFactory.Get();
enemy.SpawnOn(spawnPoint);
}
SpawnOn
— это задавать собственную позицию равной центру тайла. Так как модель префаба расположена правильно, куб-враг окажется поверх этого тайла. public void SpawnOn (GameTile tile) {
transform.localPosition = tile.transform.localPosition;
}
Enemy
общий метод GameUpdate
, возвращающий информацию о том, жив ли он, что на данном этапе всегда будет истиной. Пока просто заставим его двигаться вперёд согласно дельте времени. public bool GameUpdate () {
transform.localPosition += Vector3.forward * Time.deltaTime;
return true;
}
Game
, но давайте вместо этого изолируем его и создадим тип EnemyCollection
. Это сериализуемый класс, который ни от чего не наследует. Дадим ему общий метод для добавления врага и ещё один метод для обновления всей коллекции.using System.Collections.Generic;
[System.Serializable]
public class EnemyCollection {
List<Enemy> enemies = new List<Enemy>();
public void Add (Enemy enemy) {
enemies.Add(enemy);
}
public void GameUpdate () {
for (int i = 0; i < enemies.Count; i++) {
if (!enemies[i].GameUpdate()) {
int lastIndex = enemies.Count - 1;
enemies[i] = enemies[lastIndex];
enemies.RemoveAt(lastIndex);
i -= 1;
}
}
}
}
Game
будет достаточно создать всего одну такую коллекцию, в каждом кадре обновлять её и добавлять в неё созданных врагов. Врагов будем обновлять сразу же после возможного спаунинга нового врага, чтобы обновление происходило мгновенно. EnemyCollection enemies = new EnemyCollection();
…
void Update () {
…
enemies.GameUpdate();
}
…
void SpawnEnemy () {
…
enemies.Add(enemy);
}
GameTile
общее свойство-геттер для получения следующего тайла на пути. public GameTile NextTileOnPath => nextOnPath;
Enemy
отслеживает оба тайла, чтобы на него не влияло изменение пути. Также он будет отслеживать позиции, чтобы нам не приходилось получать их в каждом кадре, и отслеживать процесс перемещения. GameTile tileFrom, tileTo;
Vector3 positionFrom, positionTo;
float progress;
SpawnOn
. Первая точка — это тайл, из которого движется враг, а конечная точка — следующий тайл на пути. Это предполагает, что существует следующий тайл, если только враг не был создан в конечной точке, что должно быть невозможным. Тогда мы кэшируем позиции тайлов и обнулим progress. Позицию врага нам задавать здесь не нужно, потому что его метод GameUpdate
вызывается в том же кадре. public void SpawnOn (GameTile tile) {
//transform.localPosition = tile.transform.localPosition;
Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this);
tileFrom = tile;
tileTo = tile.NextTileOnPath;
positionFrom = tileFrom.transform.localPosition;
positionTo = tileTo.transform.localPosition;
progress = 0f;
}
GameUpdate
. Прибавим неизменную дельту времени, чтобы враги двигались со скоростью один тайл в секунду. Когда движение (progress) завершено, смещаем данные так, чтобы To
становилось значение From
, а новым To
— следующий тайл на пути. Затем выполняем декремент progress. Когда данные становятся актуальными, интерполируем позицию врага между From
и To
. Так как интерполятором является progress, его значение обязательно находится в интервале от 0 и 1, моэтому мы можем использовать sVector3.LerpUnclamped
. public bool GameUpdate () {
progress += Time.deltaTime;
while (progress >= 1f) {
tileFrom = tileTo;
tileTo = tileTo.NextTileOnPath;
positionFrom = positionTo;
positionTo = tileTo.transform.localPosition;
progress -= 1f;
}
transform.localPosition =
Vector3.LerpUnclamped(positionFrom, positionTo, progress);
return true;
}
From
и To
, нужно сравнивать следующий тайл на пути с null
. Если это так, то мы достигли конечной точки и враг закончил движение. Выполняем для него Reclaim и возвращаем false
. while (progress >= 1f) {
tileFrom = tileTo;
tileTo = tileTo.NextTileOnPath;
if (tileTo == null) {
OriginFactory.Reclaim(this);
return false;
}
positionFrom = positionTo;
positionTo = tileTo.transform.localPosition;
progress -= 1f;
}
GameTile.GrowPathTo
. Сделаем её доступной с помощью свойства ExitPoint
. public Vector3 ExitPoint { get; private set; }
…
GameTile GrowPathTo (GameTile neighbor) {
…
neighbor.ExitPoint =
(neighbor.transform.localPosition + transform.localPosition) * 0.5f;
return
neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null;
}
public void BecomeDestination () {
distance = 0;
nextOnPath = null;
ExitPoint = transform.localPosition;
}
Enemy
таким образом, чтобы он использовал точки выхода, а не центры тайлов. public bool GameUpdate () {
progress += Time.deltaTime;
while (progress >= 1f) {
…
positionTo = tileFrom.ExitPoint;
progress -= 1f;
}
transform.localPosition = Vector3.Lerp(positionFrom, positionTo, progress);
return true;
}
public void SpawnOn (GameTile tile) {
…
positionTo = tileFrom.ExitPoint;
progress = 0f;
}
public enum Direction {
North, East, South, West
}
GameTile
свойство, чтобы хранить направление его пути. public Direction PathDirection { get; private set; }
GrowTo
, который задаёт свойство. Так как мы выращиваем путь с конца в начало, направление будет противоположным к тому, откуда мы выращиваем путь. public GameTile GrowPathNorth () => GrowPathTo(north, Direction.South);
public GameTile GrowPathEast () => GrowPathTo(east, Direction.West);
public GameTile GrowPathSouth () => GrowPathTo(south, Direction.North);
public GameTile GrowPathWest () => GrowPathTo(west, Direction.East);
GameTile GrowPathTo (GameTile neighbor, Direction direction) {
…
neighbor.PathDirection = direction;
return
neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null;
}
GetRotation
для направления, поэтому давайте сделаем это, создав расширяющий метод. Добавим общий статический метод DirectionExtensions
, дадим ему массив для кэширования необходимых кватернионов, а также метод GetRotation
для возврата соответствующего значения направления. В данном случае имеет смысл поместить расширяющий класс в тот же файл, что и тип перечисления.using UnityEngine;
public enum Direction {
North, East, South, West
}
public static class DirectionExtensions {
static Quaternion[] rotations = {
Quaternion.identity,
Quaternion.Euler(0f, 90f, 0f),
Quaternion.Euler(0f, 180f, 0f),
Quaternion.Euler(0f, 270f, 0f)
};
public static Quaternion GetRotation (this Direction direction) {
return rotations[(int)direction];
}
}
this
. Он определяет значение типа и экземпляра, с которым будет работать метод. Такой подход обозначает, что расширяющие свойства невозможны.Enemy
при спаунинге и каждый раз, когда мы входим в новый тайл. После обновления данных тайл From
даёт нам направление. public bool GameUpdate () {
progress += Time.deltaTime;
while (progress >= 1f) {
…
transform.localRotation = tileFrom.PathDirection.GetRotation();
progress -= 1f;
}
transform.localPosition =
Vector3.LerpUnclamped(positionFrom, positionTo, progress);
return true;
}
public void SpawnOn (GameTile tile) {
…
transform.localRotation = tileFrom.PathDirection.GetRotation();
progress = 0f;
}
Direction
, потому что они малы и тесно связаны.public enum Direction {
North, East, South, West
}
public enum DirectionChange {
None, TurnRight, TurnLeft, TurnAround
}
GetDirectionChangeTo
, который возвращает смену направления от текущего направления к следующему. Если направления совпадают, то смены нет. Если следующее на один больше текущего, то это поворот направо. Но так как направления повторяются такая же ситуация будет, когда следующее на три меньше текущего. С поворотом налево будет то же самое, только сложение и вычитание поменяются местами. Единственный оставшийся случай — это поворот назад. public static DirectionChange GetDirectionChangeTo (
this Direction current, Direction next
) {
if (current == next) {
return DirectionChange.None;
}
else if (current + 1 == next || current - 3 == next) {
return DirectionChange.TurnRight;
}
else if (current - 1 == next || current + 3 == next) {
return DirectionChange.TurnLeft;
}
return DirectionChange.TurnAround;
}
public static float GetAngle (this Direction direction) {
return (float)direction * 90f;
}
Enemy
придётся отслеживать направление, смену направления и углы, между которыми нужно выполнять интерполяцию. Direction direction;
DirectionChange directionChange;
float directionAngleFrom, directionAngleTo;
SpawnOn
становится сложнее, поэтому давайте переместим код подготовки состояния в другой метод. Мы назначим исходное состояние врага как вводное состояние, поэтому назовём его PrepareIntro
. В этом состоянии враг перемещается от центра к краю своего начального тайла, поэтому смена направления не происходит. Углы From
и To
одинаковы. public void SpawnOn (GameTile tile) {
Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this);
tileFrom = tile;
tileTo = tile.NextTileOnPath;
//positionFrom = tileFrom.transform.localPosition;
//positionTo = tileFrom.ExitPoint;
//transform.localRotation = tileFrom.PathDirection.GetRotation();
progress = 0f;
PrepareIntro();
}
void PrepareIntro () {
positionFrom = tileFrom.transform.localPosition;
positionTo = tileFrom.ExitPoint;
direction = tileFrom.PathDirection;
directionChange = DirectionChange.None;
directionAngleFrom = directionAngleTo = direction.GetAngle();
transform.localRotation = direction.GetRotation();
}
GameUpdate
, переместим код изменения состояния в новый метод PrepareNextState
. Оставим только изменения тайлов From
и To
, потому что мы используем их здесь для проверки того, закончил ли враг путь. public bool GameUpdate () {
progress += Time.deltaTime;
while (progress >= 1f) {
…
//positionFrom = positionTo;
//positionTo = tileFrom.ExitPoint;
//transform.localRotation = tileFrom.PathDirection.GetRotation();
progress -= 1f;
PrepareNextState();
}
…
}
To
к From
. Поворот мы больше не задаём. void PrepareNextState () {
positionFrom = positionTo;
positionTo = tileFrom.ExitPoint;
directionChange = direction.GetDirectionChangeTo(tileFrom.PathDirection);
direction = tileFrom.PathDirection;
directionAngleFrom = directionAngleTo;
}
To
совпадает с направлением пути текущей ячейки. Кроме того, нам нужно задать поворот, чтобы враг смотрел прямо вперёд. void PrepareForward () {
transform.localRotation = direction.GetRotation();
directionAngleTo = direction.GetAngle();
}
To
должен указываться относительно текущего направления. Нам не нужно волноваться, что угол станет меньше 0° или больше 360°, потому что Quaternion.Euler
может справиться с этим. void PrepareTurnRight () {
directionAngleTo = directionAngleFrom + 90f;
}
void PrepareTurnLeft () {
directionAngleTo = directionAngleFrom - 90f;
}
void PrepareTurnAround () {
directionAngleTo = directionAngleFrom + 180f;
}
PrepareNextState
мы можем использовать switch
для смены направления, чтобы решить, какой из четырёх методов вызывать. void PrepareNextState () {
…
switch (directionChange) {
case DirectionChange.None: PrepareForward(); break;
case DirectionChange.TurnRight: PrepareTurnRight(); break;
case DirectionChange.TurnLeft: PrepareTurnLeft(); break;
default: PrepareTurnAround(); break;
}
}
GameUpdate
нам нужно проверять, произошла ли смена направления. Если да, то выполнить интерполяцию между двумя углами и задать поворот. public bool GameUpdate () {
…
transform.localPosition =
Vector3.LerpUnclamped(positionFrom, positionTo, progress);
if (directionChange != DirectionChange.None) {
float angle = Mathf.LerpUnclamped(
directionAngleFrom, directionAngleTo, progress
);
transform.localRotation = Quaternion.Euler(0f, angle, 0f);
}
return true;
}
From
и To
, на том же самом краю, по которому враг вошёл на тайл From
.Enemy
ссылку на эту модель, доступную через поле конфигурации. [SerializeField]
Transform model = default;
void PrepareForward () {
transform.localRotation = direction.GetRotation();
directionAngleTo = direction.GetAngle();
model.localPosition = Vector3.zero;
}
void PrepareTurnRight () {
directionAngleTo = directionAngleFrom + 90f;
model.localPosition = new Vector3(-0.5f, 0f);
}
void PrepareTurnLeft () {
directionAngleTo = directionAngleFrom - 90f;
model.localPosition = new Vector3(0.5f, 0f);
}
void PrepareTurnAround () {
directionAngleTo = directionAngleFrom + 180f;
model.localPosition = Vector3.zero;
}
Direction
для этого вспомогательный расширяющий метод GetHalfVector
. static Vector3[] halfVectors = {
Vector3.forward * 0.5f,
Vector3.right * 0.5f,
Vector3.back * 0.5f,
Vector3.left * 0.5f
};
…
public static Vector3 GetHalfVector (this Direction direction) {
return halfVectors[(int)direction];
}
void PrepareTurnRight () {
directionAngleTo = directionAngleFrom + 90f;
model.localPosition = new Vector3(-0.5f, 0f);
transform.localPosition = positionFrom + direction.GetHalfVector();
}
void PrepareTurnLeft () {
directionAngleTo = directionAngleFrom - 90f;
model.localPosition = new Vector3(0.5f, 0f);
transform.localPosition = positionFrom + direction.GetHalfVector();
}
void PrepareTurnAround () {
directionAngleTo = directionAngleFrom + 180f;
model.localPosition = Vector3.zero;
transform.localPosition = positionFrom;
}
GameTile.GrowPathTo
половину вектора, чтобы нам не нужен был доступ к двум позициям тайлов. neighbor.ExitPoint =
neighbor.transform.localPosition + direction.GetHalfVector();
Enemy.GameUpdate
, потому что движением занимается поворот. public bool GameUpdate () {
…
if (directionChange == DirectionChange.None) {
transform.localPosition =
Vector3.LerpUnclamped(positionFrom, positionTo, progress);
}
//if (directionChange != DirectionChange.None) {
else {
float angle = Mathf.LerpUnclamped(
directionAngleFrom, directionAngleTo, progress
);
transform.localRotation = Quaternion.Euler(0f, angle, 0f);
}
return true;
}
GameUpdate
. float progress, progressFactor;
…
public bool GameUpdate () {
progress += Time.deltaTime * progressFactor;
…
}
public bool GameUpdate () {
progress += Time.deltaTime * progressFactor;
while (progress >= 1f) {
…
//progress -= 1f;
progress = (progress - 1f) / progressFactor;
PrepareNextState();
progress *= progressFactor;
}
…
}
progress
равен единице, разделённой на эту величину. Поворот назад не должен занимать слишком много времени, поэтому удвоим progress, чтобы он занимал полсекунды. Наконец, вводное движение покрывает только половину тайла, поэтому для сохранения постоянной скорости его progress тоже нужно удвоить. void PrepareForward () {
…
progressFactor = 1f;
}
void PrepareTurnRight () {
…
progressFactor = 1f / (Mathf.PI * 0.25f);
}
void PrepareTurnLeft () {
…
progressFactor = 1f / (Mathf.PI * 0.25f);
}
void PrepareTurnAround () {
…
progressFactor = 2f;
}
void PrepareIntro () {
…
progressFactor = 2f;
}
PrepareOutro
, зададим движение вперёд, но только до центра тайла с удвоенным progress для сохранения постоянной скорости. void PrepareOutro () {
positionTo = tileFrom.transform.localPosition;
directionChange = DirectionChange.None;
directionAngleTo = direction.GetAngle();
model.localPosition = Vector3.zero;
transform.localRotation = direction.GetRotation();
progressFactor = 2f;
}
GameUpdate
не уничтожал врага слишком рано, удалим из него сдвиг тайлов. Им теперь займётся PrepareNextState
. Таким образом, проверка на null
вернёт true
только после конца завершающего состояния. public bool GameUpdate () {
progress += Time.deltaTime * progressFactor;
while (progress >= 1f) {
//tileFrom = tileTo;
//tileTo = tileTo.NextTileOnPath;
if (tileTo == null) {
OriginFactory.Reclaim(this);
return false;
}
…
}
…
}
PrepareNextState
мы начнём со сдвига тайлов. Затем после задания позиции From
, но перед заданием позиции To
будем проверять, равен ли тайл To
значению null
. Если да, то подготавливаем завершающее состояние и пропускаем остальную часть метода. void PrepareNextState () {
tileFrom = tileTo;
tileTo = tileTo.NextTileOnPath;
positionFrom = positionTo;
if (tileTo == null) {
PrepareOutro();
return;
}
positionTo = tileFrom.ExitPoint;
…
}
FloatRange
, которую мы создали в статье Object Management, Configuring Shapes, поэтому давайте её скопируем. Единственными изменениями стали добавление конструктора с одним параметром и открытие доступа к минимуму и максимуму с помощью readonly-свойств, чтобы интервал был неизменяемым.using UnityEngine;
[System.Serializable]
public struct FloatRange {
[SerializeField]
float min, max;
public float Min => min;
public float Max => max;
public float RandomValueInRange {
get {
return Random.Range(min, max);
}
}
public FloatRange(float value) {
min = max = value;
}
public FloatRange (float min, float max) {
this.min = min;
this.max = max < min ? min : max;
}
}
using UnityEngine;
public class FloatRangeSliderAttribute : PropertyAttribute {
public float Min { get; private set; }
public float Max { get; private set; }
public FloatRangeSliderAttribute (float min, float max) {
Min = min;
Max = max < min ? min : max;
}
}
FloatRangeSliderDrawer
в папку Editor.using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(FloatRangeSliderAttribute))]
public class FloatRangeSliderDrawer : PropertyDrawer {
public override void OnGUI (
Rect position, SerializedProperty property, GUIContent label
) {
int originalIndentLevel = EditorGUI.indentLevel;
EditorGUI.BeginProperty(position, label, property);
position = EditorGUI.PrefixLabel(
position, GUIUtility.GetControlID(FocusType.Passive), label
);
EditorGUI.indentLevel = 0;
SerializedProperty minProperty = property.FindPropertyRelative("min");
SerializedProperty maxProperty = property.FindPropertyRelative("max");
float minValue = minProperty.floatValue;
float maxValue = maxProperty.floatValue;
float fieldWidth = position.width / 4f - 4f;
float sliderWidth = position.width / 2f;
position.width = fieldWidth;
minValue = EditorGUI.FloatField(position, minValue);
position.x += fieldWidth + 4f;
position.width = sliderWidth;
FloatRangeSliderAttribute limit = attribute as FloatRangeSliderAttribute;
EditorGUI.MinMaxSlider(
position, ref minValue, ref maxValue, limit.Min, limit.Max
);
position.x += sliderWidth + 4f;
position.width = fieldWidth;
maxValue = EditorGUI.FloatField(position, maxValue);
if (minValue < limit.Min) {
minValue = limit.Min;
}
if (maxValue < minValue) {
maxValue = minValue;
}
else if (maxValue > limit.Max) {
maxValue = limit.Max;
}
minProperty.floatValue = minValue;
maxProperty.floatValue = maxValue;
EditorGUI.EndProperty();
EditorGUI.indentLevel = originalIndentLevel;
}
}
EnemyFactory
опцию настройки масштаба. Интервал масштабов не должен быть слишком большим, но достаточным для создания миниатюрных и гигантски разновидностей врагов. Что-нибудь в пределах 0.5–2 со стандартным значением 1. Будем выбирать случайный масштаб в этом интервале в Get
и передавать его врагу через новый метод Initialize
. [SerializeField, FloatRangeSlider(0.5f, 2f)]
FloatRange scale = new FloatRange(1f);
public Enemy Get () {
Enemy instance = CreateGameObjectInstance(prefab);
instance.OriginFactory = this;
instance.Initialize(scale.RandomValueInRange);
return instance;
}
Enemy.Initialize
просто задаёт одинаковый по всем измерениям масштаб его модели. public void Initialize (float scale) {
model.localScale = new Vector3(scale, scale, scale);
}
EnemyFactory
интервал смещений пути и будем передавать случайное смещение методу Initialize
. Смещение может быть отрицательным или положительным, но никогда не больше ?, потому что это сдвинуло бы врага на соседний тайл. Кроме того, мы не хотим, чтобы враги выходили за пределы тайлов, по которым идут, поэтому на самом деле интервал будет меньше, например, 0.4, однако истинные пределы зависят от размера врага. [SerializeField, FloatRangeSlider(-0.4f, 0.4f)]
FloatRange pathOffset = new FloatRange(0f);
public Enemy Get () {
Enemy instance = CreateGameObjectInstance(prefab);
instance.OriginFactory = this;
instance.Initialize(
scale.RandomValueInRange, pathOffset.RandomValueInRange
);
return instance;
}
Enemy
необходимо его отслеживать. float pathOffset;
…
public void Initialize (float scale, float pathOffset) {
model.localScale = new Vector3(scale, scale, scale);
this.pathOffset = pathOffset;
}
void PrepareForward () {
transform.localRotation = direction.GetRotation();
directionAngleTo = direction.GetAngle();
model.localPosition = new Vector3(pathOffset, 0f);
progressFactor = 1f;
}
void PrepareTurnRight () {
directionAngleTo = directionAngleFrom + 90f;
model.localPosition = new Vector3(pathOffset - 0.5f, 0f);
transform.localPosition = positionFrom + direction.GetHalfVector();
progressFactor = 1f / (Mathf.PI * 0.25f);
}
void PrepareTurnLeft () {
directionAngleTo = directionAngleFrom - 90f;
model.localPosition = new Vector3(pathOffset + 0.5f, 0f);
transform.localPosition = positionFrom + direction.GetHalfVector();
progressFactor = 1f / (Mathf.PI * 0.25f);
}
void PrepareTurnAround () {
directionAngleTo = directionAngleFrom + 180f;
model.localPosition = new Vector3(pathOffset, 0f);
transform.localPosition = positionFrom;
progressFactor = 2f;
}
void PrepareIntro () {
…
model.localPosition = new Vector3(pathOffset, 0f);
transform.localRotation = direction.GetRotation();
progressFactor = 2f;
}
void PrepareOutro () {
…
model.localPosition = new Vector3(pathOffset, 0f);
transform.localRotation = direction.GetRotation();
progressFactor = 2f;
}
void PrepareTurnRight () {
…
progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f - pathOffset));
}
void PrepareTurnLeft () {
…
progressFactor = 1f / (Mathf.PI * 0.5f * (0.5f + pathOffset));
}
void PrepareTurnAround () {
directionAngleTo = directionAngleFrom + (pathOffset < 0f ? 180f : -180f);
model.localPosition = new Vector3(pathOffset, 0f);
transform.localPosition = positionFrom;
progressFactor =
1f / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f));
}
EnemyFactory
и будем передавать значение созданному экземпляру врага. Сделаем его вторым аргументом метода Initialize
. Враги не должны быть слишком медленными или быстрыми, чтобы игра не стала тривиально простой или невозможно трудной. Давайте ограничим интервал в пределах 0.2–5. Скорость выражается в единицах в секунду, что соответствует тайлам в секунду только при движении вперёд. [SerializeField, FloatRangeSlider(0.2f, 5f)]
FloatRange speed = new FloatRange(1f);
[SerializeField, FloatRangeSlider(-0.4f, 0.4f)]
FloatRange pathOffset = new FloatRange(0f);
public Enemy Get () {
Enemy instance = CreateGameObjectInstance(prefab);
instance.OriginFactory = this;
instance.Initialize(
scale.RandomValueInRange,
speed.RandomValueInRange,
pathOffset.RandomValueInRange
);
return instance;
}
Enemy
должен отслеживать и скорость. float speed;
…
public void Initialize (float scale, float speed, float pathOffset) {
model.localScale = new Vector3(scale, scale, scale);
this.speed = speed;
this.pathOffset = pathOffset;
}
void PrepareForward () {
…
progressFactor = speed;
}
void PrepareTurnRight () {
…
progressFactor = speed / (Mathf.PI * 0.5f * (0.5f - pathOffset));
}
void PrepareTurnLeft () {
…
progressFactor = speed / (Mathf.PI * 0.5f * (0.5f + pathOffset));
}
void PrepareTurnAround () {
…
progressFactor =
speed / (Mathf.PI * Mathf.Max(Mathf.Abs(pathOffset), 0.2f));
}
void PrepareIntro () {
…
progressFactor = 2f * speed;
}
void PrepareOutro () {
…
progressFactor = 2f * speed;
}
К сожалению, не доступен сервер mySQL