小猪学U3D—塔防-敌人(TowerDefense-Enemies)

上一节,我们讲解了如何创建游戏地图、目的地和墙的放置、寻路。本节将重点学习如何在地图中添加出生点、敌人,并让敌人沿着规划路径前往离他最近的目的地。

敌人(Enemies)

1.出生点(Spawn Points)

在产生敌人之前,我们需要确定将敌人放置在板上的哪个位置。 所以需要创建一个出生点。

1.1.瓦片内容(Tile Content)

a.生成点是瓦片内容的另一种类型,将其添加到GameTileContentType

// 瓦片内容类型
public enum GameTileContentType{
  Empty,  //路
  Destination,  //目的地
  Wall,  //墙
  SpawnPoint  //敌人生成点
}

b.创建一个预制件以使其可视化(参考目的地预制件,改个颜色就行)

c.将对出生点的支持添加到内容工厂,并为其提供对预制件的引用。

// 瓦片内容工厂
public class GameTileContentFactory : ScriptableObject
{
    ...
    [SerializeField]
    GameTileContent spawnPointPrefab = default;  // 生成点预制件
    ...
    // 生成不同类型瓦片
    public GameTileContent Get(GameTileContentType type){
      switch(type){
         ...
        case GameTileContentType.SpawnPoint:
          return Get(spawnPointPrefab);
      }
      return null;
    }
    ...
}

1.2.切换出生点(Toggling Spawn Points)

a.与其他切换方法一样,添加一种将生成点切换到GameBoard的方法,并设置一个默认的出生点

游戏只有在有敌人的情况下才有意义,这就需要有出生点。 因此,有效的游戏面板应至少包含一个出生点。 添加敌人时,我们稍后还需要访问出生点,因此使用列表来跟踪所有带有出生点的瓦片。 切换出生点时更新列表,并防止删除最后一个出生点。

// 游戏面板对象
public class GameBoard : MonoBehaviour
{
    ...
    List<GameTile> spawnPoints = new List<GameTile>();  //出生点列表

    // 初始化方法
    public void Initialize(Vector2Int size, GameTileContentFactory contentFactory){
      ...
      // 设置出生点
      ToggleSpawnPoint(tiles[0]);
    }
    ...
    // 对调瓦片的生成点<->非生成点属性
    public void ToggleSpawnPoint(GameTile tile){
      if(tile == null){
        return;
      }
      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); //添加到出生点列表
      }
    }
    ...
}

b.在Game中添加鼠标右键+Shit事件,用于设置出生点

// 游戏对象
public class Game : MonoBehaviour
{
    ...
    void Update(){
      if(Input.GetMouseButtonDown(1)){  //鼠标右键事件
        if(Input.GetKey(KeyCode.LeftShift)){  //同时按下了Shift
          board.ToggleSpawnPoint(board.GetTile(TouchRay));  //切换空瓦片和出生地
        }else{
          board.ToggleDestination(board.GetTile(TouchRay));  //切换空瓦片和目的地
        }
      }
      ...
    }
}

c.运行并测试添加出生点

1.3.访问出生点(Accessing Spawn Points)

// 游戏面板对象
public class GameBoard : MonoBehaviour
{
    ...
    // 获取单个出生点
    public GameTile GetSpawnPoint(int i){
      return spawnPoints[i];
    }
    // 获取出生点总数
    public int SpawnPointCount => spawnPoints.Count;
}

2.生成敌人(Spawning Enemies)

生成敌人有点像创建瓦片内容。 我们通过工厂创建一个预制实例,然后将其放在板上。

2.1.工厂(Factories)

a.创建通用游戏对象工厂,并替换原瓦片内容工厂的MoveToFactoryScene对象移入场景方法

using System.Collections;
using System.Collections.Generic;
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 o = Instantiate(prefab);
    SceneManager.MoveGameObjectToScene(o.gameObject, scene);
    return o;
  }
}


using UnityEngine;
//using UnityEngine.SceneManagement;

// 瓦片内容工厂
[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) {
	//	…
	//}
}

b.创建EnemyFactory,通过Get方法实例化一个敌人预制件,以及一个相应的回收方法。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 敌人预制件工厂
[CreateAssetMenu] //添加到unity create菜单
public class EnemyFactory : GameObjectFactory{
    [SerializeField]
    Enemy prefab = default;

    // 生成敌人
    public Enemy Get(){
      Enemy o = CreateGameObjectInstance(prefab);
      o.OriginFactory = this;
      return o;
    }

    // 回收敌人
    public void Reclaim(Enemy o){
      Debug.Assert(o.OriginFactory == this, "Wrong factory reclaimed!");
      Destroy(o.gameObject);
    }
}

c.新的敌人类型仅需要追踪其原始工厂

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 敌人
public class Enemy : MonoBehaviour
{
    EnemyFactory originFactory;

    public EnemyFactory OriginFactory{
      get => originFactory;
      set {
        Debug.Assert(originFactory == null, "Redefined origin factory!");
        originFactory = value;
      }
    }
}

2.2.预制体(Prefab)

敌人需要可视化,并且可以是任何东西。 我们将使用机器人,蜘蛛,鬼魂或诸如立方体之类的简单对象。 但总的来说,敌人拥有任意复杂的3D模型。

a.为敌人的预制层创建一个根对象,该根对象仅附加了Enemy组件。

b.给该对象一个Model子节点,即模型根

模型根的目的是相对于敌人的局部原点定位3D模型,因此将其视为其站立或悬停在其上方的枢轴点。 c.本例中,模型是默认比例的立方体,我将其设置为深蓝色。 使它成为模型根的子节点,并将其Y位置设置为0.25,以便它位于地面上

因此,敌人的预制件由三个嵌套对象组成:预制根,模型根和立方体。 对于简单的立方体而言,这可以认为是过渡设计了,但它可以移动和设置任何敌人的动画而不用担心其细节。

d.通过CreateAssetMenu创建一个敌人工厂并将预制件分配给它

2.3.添加敌人到地图(Placing Enemies on the Board)

为了将敌人放在面板上,游戏需要引用敌人工厂。 由于我们将需要大量敌人,因此还添加了一个生成速度的配置选项,以每秒敌人数表示。 0.1-10的范围似乎是合理的,默认值为1。

通过将速度乘以时间增量来跟踪Update中的生成进度。 如果进度超过1,则递减并通过新的SpawnEnemy方法生成敌人。 只要进度超过1,就继续执行此操作,以防速度过快且帧时间结束得太长,而产生多个敌人。

// 游戏对象
public class Game : MonoBehaviour{
    ...
    float spawnProcess;                   //敌人生成进度
    ...
    void Update(){
        ...
        // 生成敌人
        spawnProcess += spawnSpeed * Time.deltaTime;
        while(spawnProcess >= 1f){
          spawnProcess -= 1f;
          SpawnEnemy();
        }
    }
    // 生成敌人
    void SpawnEnemy(){
      GameTile spawnPoint = board.GetSpawnPoint(Random.Range(0, board.SpawnPointCount));
      Enemy enemy = enemyFactory.Get();
      enemy.SpawnOn(spawnPoint);
    }
    ...
}

让SpawnEnemy从棋盘上随机获得一个生成点,并在该图块上生成一个敌人。 我们将为敌人提供一个SpawnOn方法以正确定位自身。

// 敌人
public class Enemy : MonoBehaviour
{
    ...
    // 设置敌人到出生瓦片位置
    public void SpawnOn(GameTile tile){
      transform.localPosition = tile.transform.localPosition;
    }
}

 

3.移动敌人(Moving Enemies)

一旦敌人出现,它应该开始沿着路径移动到最近的目的地。 我们必须为它们设置动画,以实现这一目标。 我们首先简单地将它们在图块之间滑动,然后使它们的移动更加复杂。

3.1.敌人集合(Enemy Collection)

a.Game添加敌人集合,并在Update中自动移动敌人

// 游戏对象
public class Game : MonoBehaviour{
    ...
    EnemyCollection enemies = new EnemyCollection();  //敌人集合(用于控制移动等)
    ...
    void Update(){
      ...
      // 更新敌人位置等信息
      enemies.GameUpdate();
    }
    // 生成敌人
    void SpawnEnemy(){
      ...
      enemies.Add(enemy); //添加到敌人集合中用于后续跟踪
    }
}

b.添加EnemyCollection敌人集合,增加批量移动敌人方法

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 敌人集合
[System.Serializable]
public class EnemyCollection{
    List<Enemy> enemies = new List<Enemy>();

    // 添加敌人到集合
    public void Add(Enemy o){
      enemies.Add(o);
    }

    // 更新集合中所有敌人的位置等信息
    public void GameUpdate(){
      for(int i=0; i<enemies.Count; i++){
        if(!enemies[i].GameUpdate()){ // 敌人嗝屁了,从集合中移除并移动尾部索引减少碎片
          int lastIdx = enemies.Count - 1;
          enemies[i] = enemies[lastIdx];
          enemies.RemoveAt(lastIdx);
        }
      }
    }
}

c.敌人增加向前移动方法

// 敌人
public class Enemy : MonoBehaviour
{
    ...
    // 更新敌人位置等信息(敌人死亡时返回false)
    public bool GameUpdate(){
      transform.localPosition += Vector3.forward * Time.deltaTime;  //根据时间向前移动
      return true;
    }
}

d.运行,Shift+鼠标右键添加多个敌人出生点,可以看到敌人出生后固定向北移动

3.2.跟随路径(Following the Path)

我们的敌人正在前进,但他们还没有沿着路径前行。为了实现这一目标,敌人必须知道下一步要去哪里。

a.给GameTile一个公共getter属性来检索路径上的下一个瓦片

// 瓦片对象
public class GameTile : MonoBehaviour{
    ...
    public GameTile NextTileOnPath => nextOnPath; //路径下一瓦片属性允许公开访问
    ...
}

b.让敌人追踪两个瓦片,这样它就不会受到路径变化的影响。还要追踪位置,这样我们就不必在每一帧中检索它们。它也需要追踪进度。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 敌人
public class Enemy : MonoBehaviour{
    ...
    // 位置追踪
    GameTile fromTile, toTile;    //瓦片从哪里来,下一步要去哪里
    Vector3 fromPos, toPos;       //同上的坐标
    float progress;               //进度


    // 设置敌人到出生瓦片位置
    public void SpawnOn(GameTile tile){
      //transform.localPosition = tile.transform.localPosition;
      Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this);
      // 位置跟踪属性设置
      fromTile = tile;
      fromPos = fromTile.transform.localPosition;
      toTile = tile.NextTileOnPath;
      toPos = toTile.transform.localPosition;
      progress = 0f;
    }

    // 更新敌人位置等信息(敌人死亡时返回false)
    public bool GameUpdate(){
      //transform.localPosition += Vector3.forward * Time.deltaTime;  //根据时间向前移动
      progress += Time.deltaTime;
      while(progress >= 1f){
        if(toTile.NextTileOnPath == null){  //没有下一瓦片,销毁敌人
          OriginFactory.Reclaim(this);
          return false;
        }
        // 更新位置跟踪关系
        fromTile = toTile;
        fromPos = toPos;
        toTile = toTile.NextTileOnPath;
        toPos = toTile.transform.localPosition;
        progress -= 1f;
      }
      transform.localPosition = Vector3.LerpUnclamped(fromPos, toPos, progress);  //为了确保移动的平顺性,我们使用进度插值
      return true;
    }
}

c.运行后可以看到敌人会沿着规划路径平滑移动,当然你也试下添加多个敌人出生点、目的地,并用墙阻挡他们

3.3.边到边平滑转弯(Going From Edge to Edge)

仔细观察,你会发现目前敌人在转向时,会先移动到瓦片中心后突然直角改变方向。如果敌人可以在瓦片间直接斜着或绕弧形移动会更流畅些。我们可以通过平均相邻瓦片的位置来找到它们之间的边缘点来实现。

a.通过ExitPoint属性记录下一路径相邻瓦片边缘点

// 瓦片对象
public class GameTile : MonoBehaviour
{
    ...
    public Vector3 ExitPoint { get; private set;} //根据下一瓦片的位置,确定敌人离开当前瓦片时的离开点
    ...
    // 设置为目的地
    public void BecameDestination(){
      ...
      ExitPoint = transform.localPosition;  //目的地的出口点在其中心点
    }

    // 反向设置路径关系(从目的地瓦片开始)
    GameTile GrowPathTo(GameTile neighbor){
      ...
      neighbor.ExitPoint = (neighbor.transform.localPosition + transform.localPosition) * 0.5f; //平均相邻瓦片的位置来找到它们之间的边缘点
      return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; 
    }
    ...
}

b.调整敌人的行走目的地为瓦片边缘点交接

// 敌人
public class Enemy : MonoBehaviour
{
    ...
    // 设置敌人到出生瓦片位置
    public void SpawnOn(GameTile tile){
      ...
      toPos =  fromTile.ExitPoint;  //当前瓦片与下一瓦片的位置边缘点中心点 //toTile.transform.localPosition;
      progress = 0f;
    }

    // 更新敌人位置等信息(敌人死亡时返回false)
    public bool GameUpdate(){
      ...
        toPos = fromTile.ExitPoint; //当前瓦片与下一瓦片的位置边缘点中心点  //toTile.transform.localPosition;
        progress -= 1f;
      }
      ...
    }
}

c.运行后可以看到,敌人在转弯时已经变得平滑

3.4.转向(Orientation)

目前敌人能沿着道路前进,但从未改变过朝向。从上图可以看到当敌人由于路径变化而转身时,它们会保持静止一秒钟后直接倒退。假如我们把方块改为怪兽的话,你一定会看到怪兽屁股朝前,倒着走路。我们需要增加保持一面朝前的的机制,就必须知道敌人沿路径前进的方向,我们在敌人中实现朝向的属性和转向的机制。

a.添加朝向枚举和方向旋转角度等扩展方法

using UnityEngine;

// 朝向
public enum Direction{  //方向枚举
  North, East, South, West
}

// 转向动作
public enum DirectionChange{  //转向动作枚举
  None, TurnRight, TurnLeft, TurnAround
}

// 方向类方法扩展类(类内部的static方法可自动扩展到任何对象上使用)
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];
    }

    // 获取从一个方向到下一个方向的转向动作
    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;
    }
}

b.瓦片增加路径朝向属性

// 瓦片对象
public class GameTile : MonoBehaviour
{
    ...
    // 路径朝向
    public Direction PathDirection { get; private set;} //下一瓦片的方向
    ...
    // 反向设置路径关系(从目的地瓦片开始)
    GameTile GrowPathTo(GameTile neighbor, Direction direction){
      ...
      neighbor.PathDirection = direction;   //路径朝向
      return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; 
    }
    // 沿特定方向扩展路径
    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);
    ...
}

c.敌人出生和更新时旋转敌人朝向角度

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 敌人
public class Enemy : MonoBehaviour
{
    // 工厂反向引用
    EnemyFactory originFactory;
    public EnemyFactory OriginFactory{
      get => originFactory;
      set {
        Debug.Assert(originFactory == null, "Redefined origin factory!");
        originFactory = value;
      }
    }
    // 位置追踪
    GameTile tileFrom, tileTo;    //瓦片从哪里来,下一步要去哪里
    Vector3 positionFrom, positionTo;       //同上的坐标
    float progress;               //进度
    // 方向跟踪
    Direction direction;          //方向
    DirectionChange directionChange;  //转向动作
    float directionAngleFrom, directionAngleTo; //方向度数


    // 设置敌人到出生瓦片位置
    public void SpawnOn(GameTile tile){
      //transform.localPosition = tile.transform.localPosition;
      Debug.Assert(tile.NextTileOnPath != null, "Nowhere to go!", this);
      // 位置跟踪属性设置
      tileFrom = tile;
      tileTo = tile.NextTileOnPath;
      progress = 0f;
      // 初始化敌人准备状态
      PrepareIntro();
    }

    // 敌人准备状态
    void PrepareIntro(){
      positionFrom = tileFrom.transform.localPosition;
      positionTo =  tileFrom.ExitPoint;  //当前瓦片与下一瓦片的位置边缘点中心点 //tileTo.transform.localPosition;
      direction = tileFrom.PathDirection; //路径方向
      directionChange = DirectionChange.None; //初始状态下,敌人会从起始瓦片的中心移动到其边缘,因此不会发生方向变化
      directionAngleFrom = directionAngleTo = direction.GetAngle(); //获取方向角度数
      transform.localRotation =  direction.GetRotation();  //旋转敌人的朝向旋转角度
    }

    // 更新敌人位置等信息(敌人死亡时返回false)
    public bool GameUpdate(){
      //transform.localPosition += Vector3.forward * Time.deltaTime;  //根据时间向前移动
      progress += Time.deltaTime;
      while(progress >= 1f){
        if(tileTo.NextTileOnPath == null){  //没有下一瓦片,销毁敌人
          OriginFactory.Reclaim(this);
          return false;
        }
        // 更新位置跟踪关系
        tileFrom = tileTo;
        tileTo = tileTo.NextTileOnPath;
        progress -= 1f;
        // 更新敌人位置、方向等
        PrepareNextState();
      }
      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;
    }

    // 敌人行走中,更新敌人下一桢位置、方向等
    void PrepareNextState(){
      positionFrom = positionTo;
      positionTo = tileFrom.ExitPoint; //当前瓦片与下一瓦片的位置边缘点中心点  //tileTo.transform.localPosition;
      directionChange = direction.GetDirectionChangeTo(tileFrom.PathDirection);
      direction = tileFrom.PathDirection; //路径方向
      directionAngleFrom = directionAngleTo;   //方向度数
      //旋转角度
      switch(directionChange){
        case DirectionChange.None: PrepareForward(); break;
        case DirectionChange.TurnRight: PrepareTurnRight(); break;
        case DirectionChange.TurnLeft: PrepareTurnLeft(); break;
        default: PrepareTurnAround(); break;
      }
    }

    // 调整旋转角度
    void PrepareForward(){
      transform.localRotation =  direction.GetRotation();  //旋转敌人的朝向旋转角度
      directionAngleTo = direction.GetAngle();   //方向度数
    }
    // 万一转弯,我们不会立即旋转。 相反,必须插值到另一个角度:向右转90°,向左转90°,转弯时多180°。 To角度必须相对于当前方向,以防止由于缠绕角度而以错误的方式旋转。 我们不必担心会低于0°或高于360°,因为四元数。Euler可以解决这个问题。
    void PrepareTurnRight(){
      directionAngleTo = directionAngleFrom + 90f;   //方向度数
    }
    void PrepareTurnLeft(){
      directionAngleTo = directionAngleFrom - 90f;   //方向度数
    }
    void PrepareTurnAround(){
      directionAngleTo = directionAngleFrom + 180f;   //方向度数
    }
}

d.运行可以看到已经可以看到旋转掉头效果。不过如果遇到频繁左右转向时,会有点摇头摆尾…

4.可变敌人(Enemy Variety)

我们有一群敌人,它们都是相同的立方体,以相同的速度移动。 结果可能看起来像是一条长长的蛇,而不是单个敌人。 让我们通过随机化它们的大小,偏移量和速度使它们更加独特。

4.1.可变随机(Float Range)

随机类准备。

using System.Collections;
using System.Collections.Generic;
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;
	}
}


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;
	}
}
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;
	}
}

4.2.模型缩放(Model Scale)

我们首先调整敌人的缩放。 将比例配置选项添加到EnemyFactory。 比例范围别太大,能创建敌人的微型和巨型版本即可, 类似于0.5–1.5,默认设置为1。在Get的此范围内选择一个随机比例,并通过新的Initialize方法将其传递给敌人。

// 敌人预制件工厂
public class EnemyFactory : GameObjectFactory{
    ...
    // 缩放比例范围
    [SerializeField, FloatRangeSlider(0.5f, 2f)]
    FloatRange scale = new FloatRange(1f);

    // 生成敌人
    public Enemy Get(){
      ...
      o.Initialize(scale.RandomValueInRange); //随机大小
      return o;
    }
    ...
}

// 敌人
public class Enemy : MonoBehaviour{
    ...
    // 初始化
    public void Initialize(float scale){
      model.localScale = new Vector3(scale, scale, scale);
    }
    ...
}

今天我们重点学习了如何在地图中添加出生点、敌人,并让敌人沿着规划路径前往离他最近的目的地,并实现了转弯和掉头动作的插值平滑等。

下一次,我们尝试创建防御塔来攻击行走的敌人,好期待~!

yan 21.8.15

参考:

Unity Tower Defense – Enemies

Unity Demo教程系列——Unity塔防游戏-敌人

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。