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
. But spawn points do not affect the search for the path, so after the change we don’t need to look for new paths. 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
method should now set a spawn point to create the initial correct field state. Let's just turn on the first tile, which is in the bottom left corner. public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … ToggleDestination(tiles[tiles.Length / 2]); ToggleSpawnPoint(tiles[0]); }
Input.GetKey
method) the state of the end point will be switched void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleDestination(tile); } else { board.ToggleSpawnPoint(tile); } } }
GetSpawnPoint
with an index parameter. public GameTile GetSpawnPoint (int index) { return spawnPoints[index]; }
public int SpawnPointCount => spawnPoints.Count;
GameObjectFactory
base class. We need only one method CreateGameObjectInstance
with a common prefab parameter that creates and returns an instance, as well as controls the entire scene. Let's make the protected
method, that is, it will be available only to the class and all types that inherit from it. This is all that the class does; it is not intended to be used as a fully functional factory. Therefore, we mark it as abstract
, which will not allow creating instances of its objects. 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
so that it GameTileContentFactory
this type of factory and uses CreateGameObjectInstance
in its Get
method, and then remove the scene control code from it. 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
, which creates an instance of one prefab of Enemy
using the Get
method along with the accompanying Reclaim
method. 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
initially only needs to track its original factory. using UnityEngine; public class Enemy : MonoBehaviour { EnemyFactory originFactory; public EnemyFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } }
Enemy
component is attached to the hierarchy of the enemy's prefab.Game
must receive a link to the enemy factory. Since we need a lot of enemies, we add a configuration option to adjust the speed of spacing, expressed in the number of enemies per second. The 0.1–10 interval with a value of 1 by default seems acceptable. [SerializeField] EnemyFactory enemyFactory = default; [SerializeField, Range(0.1f, 10f)] float spawnSpeed = 1f;
Update
, increasing it by the speed multiplied by the time delta. If the value of prggress exceeds 1, then decrement it and spawn the enemy using the new SpawnEnemy
method. We continue to do this until progress exceeds 1 in case the speed is too high and the frame time is very long so that several enemies are not created at the same time. float spawnProgress; … void Update () { … spawnProgress += spawnSpeed * Time.deltaTime; while (spawnProgress >= 1f) { spawnProgress -= 1f; SpawnEnemy(); } }
SpawnEnemy
get a random spawn point from the field and create an enemy in this tile. We will give Enemy
the SpawnOn
method SpawnOn
that he can position himself correctly. void SpawnEnemy () { GameTile spawnPoint = board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount)); Enemy enemy = enemyFactory.Get(); enemy.SpawnOn(spawnPoint); }
SpawnOn
has to do is set its own position equal to the center of the tile. Since the prefab model is positioned correctly, the cube-enemy will be on top of this tile. public void SpawnOn (GameTile tile) { transform.localPosition = tile.transform.localPosition; }
Enemy
common GameUpdate
method that returns information about whether it is alive, which at this stage will always be true. For now, just make it move forward according to the time delta. public bool GameUpdate () { transform.localPosition += Vector3.forward * Time.deltaTime; return true; }
Game
, but let's isolate it instead and create an EnemyCollection
type. This is a serializable class that does not inherit from anything. We give him a general method for adding an enemy and another method for updating the entire collection. 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
will be enough to create just one such collection, update it in each frame and add created enemies to it. Enemies will be updated immediately after a possible spawning of a new enemy, so that the update takes place instantly. EnemyCollection enemies = new EnemyCollection(); … void Update () { … enemies.GameUpdate(); } … void SpawnEnemy () { … enemies.Add(enemy); }
GameTile
common getter property to get the next tile on the path. public GameTile NextTileOnPath => nextOnPath;
Enemy
keep track of both tiles so that it will not be affected by a change in the path. He will also track positions so that we do not have to receive them every frame, and track the process of moving. GameTile tileFrom, tileTo; Vector3 positionFrom, positionTo; float progress;
SpawnOn
. The first point is the tile from which the enemy is moving, and the end point is the next tile on the way. This assumes that the next tile exists, unless the enemy was created at the end point, which should be impossible. Then we cache the tile positions and reset the progress. We do not need to set the position of the enemy here, because his GameUpdate
method GameUpdate
called in the same frame. 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
. Let us add the invariable time delta, so that the enemies move at a speed of one tile per second. When the progress is completed, shift the data so that To
becomes the value From
, and the new To
next tile on the way. Then execute the decrement progress. When the data becomes relevant, we interpolate the position of the enemy between From
and To
. Since progress is the interpolator, its value is necessarily between 0 and 1, so we can use s Vector3.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
and To
, you need to compare the next tile on the way with null
. If so, then we reached the end point and the enemy finished the movement. We execute Reclaim for it and return 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
. Make it available using the ExitPoint
property. 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
so that it uses exit points, not tile centers. 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
property to keep the direction of its path. public Direction PathDirection { get; private set; }
GrowTo
, which sets the property. As we grow the path from the end to the beginning, the direction will be opposite to where we grow the path. 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
for direction, so let's do this by creating an extension method. Let's add the general static DirectionExtensions
method, give it an array for caching the necessary quaternions, and also the GetRotation
method for returning the corresponding direction value. In this case, it makes sense to put the extension class in the same file as the enumeration type. 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
. It determines the value of the type and instance with which the method will work. This approach means that expanding properties are not possible.Enemy
while spawning and every time we enter a new tile. After updating the data, the From
Tile gives us direction. 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
, because they are small and closely related. public enum Direction { North, East, South, West } public enum DirectionChange { None, TurnRight, TurnLeft, TurnAround }
GetDirectionChangeTo
, which returns the change of direction from the current direction to the next. If the directions are the same, then there is no change. If the next one is greater than the current one, then this is a right turn. But since the directions are repeated the same situation will be when the next is three less than the current one. Turning to the left will be the same, only addition and subtraction will be swapped. The only remaining case is a turn back. 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
to track the direction, change the direction and angles between which you need to perform interpolation. Direction direction; DirectionChange directionChange; float directionAngleFrom, directionAngleTo;
SpawnOn
it gets harder, so let's move the state preparation code to another method. We will assign the initial state of the enemy as an introductory state, so we will call it PrepareIntro
. In this state, the enemy moves from the center to the edge of his initial tile, so the change of direction does not occur. The angles From
and To
the same. 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
, move the state code to a new method PrepareNextState
. We leave only the changes of the tiles From
and To
, because we use them here to check whether the enemy has finished the path. public bool GameUpdate () { progress += Time.deltaTime; while (progress >= 1f) { … //positionFrom = positionTo; //positionTo = tileFrom.ExitPoint; //transform.localRotation = tileFrom.PathDirection.GetRotation(); progress -= 1f; PrepareNextState(); } … }
To
to From
. Rotation, we no longer ask. void PrepareNextState () { positionFrom = positionTo; positionTo = tileFrom.ExitPoint; directionChange = direction.GetDirectionChangeTo(tileFrom.PathDirection); direction = tileFrom.PathDirection; directionAngleFrom = directionAngleTo; }
To
coincides with the direction of the current cell path. In addition, we need to set a turn for the enemy to look straight ahead. void PrepareForward () { transform.localRotation = direction.GetRotation(); directionAngleTo = direction.GetAngle(); }
To
should be indicated relative to the current direction. We do not need to worry that the angle will become less than 0 ° or more than 360 °, because it Quaternion.Euler
can cope with this. void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; }
PrepareNextState
we can use switch
to change directions to decide which of the four methods to call. void PrepareNextState () { … switch (directionChange) { case DirectionChange.None: PrepareForward(); break; case DirectionChange.TurnRight: PrepareTurnRight(); break; case DirectionChange.TurnLeft: PrepareTurnLeft(); break; default: PrepareTurnAround(); break; } }
GameUpdate
we need to check if the change of direction has occurred. If yes, then interpolate between two angles and set a turn. 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
and To
, on the same edge, along which the enemy entered the tile From
.Enemy
link to this model, accessible through the configuration field. [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
for this auxiliary extension method 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
half of the vector so that we do not need access to two tile positions. neighbor.ExitPoint = neighbor.transform.localPosition + direction.GetHalfVector();
Enemy.GameUpdate
, because the movement is engaged in turning. 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
equals one divided by this value. Turning back should not take too much time, so double the progress so that it takes half a second. Finally, the introductory movement covers only half of the tile, so to maintain a constant speed, its progress also needs to be doubled. 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
, set forward movement, but only to the center of the tile with doubled progress to maintain a constant speed. void PrepareOutro () { positionTo = tileFrom.transform.localPosition; directionChange = DirectionChange.None; directionAngleTo = direction.GetAngle(); model.localPosition = Vector3.zero; transform.localRotation = direction.GetRotation(); progressFactor = 2f; }
GameUpdate
not to destroy the enemy too soon, we will remove the tile shift from it. He will now be engaged PrepareNextState
. Thus, checking for null
returns true
only after the end of the terminating state. public bool GameUpdate () { progress += Time.deltaTime * progressFactor; while (progress >= 1f) { //tileFrom = tileTo; //tileTo = tileTo.NextTileOnPath; if (tileTo == null) { OriginFactory.Reclaim(this); return false; } … } … }
PrepareNextState
we will start with the shift of the tiles. Then after setting the position From
, but before setting the position, To
we will check whether the tile is equal to the To
value null
. If so, prepare the final state and skip the rest of the method. void PrepareNextState () { tileFrom = tileTo; tileTo = tileTo.NextTileOnPath; positionFrom = positionTo; if (tileTo == null) { PrepareOutro(); return; } positionTo = tileFrom.ExitPoint; … }
FloatRange
we created in the article Object Management, Configuring Shapes will be useful here , so let's copy it. The only changes were the addition of a constructor with one parameter and the opening of access to the minimum and maximum using readonly-properties so that the interval was immutable. 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
to the Editor folder . 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
the scale option. The range of scales should not be too large, but sufficient to create miniature and gigantic varieties of enemies. Anything within 0.5–2 with a standard value of 1. We will choose a random scale in this interval in Get
and transfer it to the enemy through a new method 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
simply sets the scale of its model that is the same for all dimensions. public void Initialize (float scale) { model.localScale = new Vector3(scale, scale, scale); }
EnemyFactory
path to the interval offsets and pass the random offset to the method Initialize
. The offset may be negative or positive, but never more than ½, because it would move the enemy to the next tile. In addition, we do not want the enemies to go beyond the tiles on which they are moving, therefore, in fact, the interval will be less, for example, 0.4, but the true limits depend on the size of the enemy. [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
it is necessary to track it. 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
and we will transfer the value to the created instance of the enemy. We make it the second argument of the method Initialize
. Enemies should not be too slow or fast so that the game does not become trivially simple or impossibly difficult. Let's limit the interval to 0.2–5. Speed is expressed in units per second, which corresponds to tiles per second only when moving forward. [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
must track and speed. 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; }
Source: https://habr.com/ru/post/452756/
All Articles