
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; } Enemyto track the direction, change the direction and angles between which you need to perform interpolation. Direction direction; DirectionChange directionChange; float directionAngleFrom, directionAngleTo; SpawnOnit 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 Fromand Tothe 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 Fromand 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(); } … } Toto From. Rotation, we no longer ask. void PrepareNextState () { positionFrom = positionTo; positionTo = tileFrom.ExitPoint; directionChange = direction.GetDirectionChangeTo(tileFrom.PathDirection); direction = tileFrom.PathDirection; directionAngleFrom = directionAngleTo; } Tocoincides 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(); } Toshould 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.Eulercan cope with this. void PrepareTurnRight () { directionAngleTo = directionAngleFrom + 90f; } void PrepareTurnLeft () { directionAngleTo = directionAngleFrom - 90f; } void PrepareTurnAround () { directionAngleTo = directionAngleFrom + 180f; } PrepareNextStatewe can use switchto 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; } } GameUpdatewe 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; } Fromand To, on the same edge, along which the enemy entered the tile From.
Enemylink 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; } Directionfor 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.GrowPathTohalf 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; } … } progressequals 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; } GameUpdatenot to destroy the enemy too soon, we will remove the tile shift from it. He will now be engaged PrepareNextState. Thus, checking for nullreturns trueonly 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; } … } … } PrepareNextStatewe will start with the shift of the tiles. Then after setting the position From, but before setting the position, Towe will check whether the tile is equal to the Tovalue 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; … } FloatRangewe 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; } } FloatRangeSliderDrawerto 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; } } EnemyFactorythe 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 Getand 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.Initializesimply 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); } 

EnemyFactorypath 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; } Enemyit 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)); } EnemyFactoryand 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; } Enemymust 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