Unity 實現簡單的關卡管理系統
今天我們嘗試實現一個簡單的關卡管理系統。
先想想關卡都需要什麼功能?
在我基本的考慮下主要有:
1.投放物體(主角,道具,怪物)
2.控制關卡流程(比如,小關卡順序,怪物波次)
3.觸發各種事件(比如觸發劇情,啟用技能,啟用碰撞牆等等)
OK,知道我們需要什麼了,那我們就開始考慮要怎麼製作了。
在一個大關卡中,往往會劃分多個小的戰鬥區,玩家要控制角色依次通過戰鬥區域,獲得勝利,所以我們首要目標是要實現這些小的戰鬥區。
目標確定,那需要考慮一下戰鬥區一般的形狀:
1.矩形
2.圓形
3.不規則多邊形(這個實現較為複雜,暫且不考慮)
4.扇形(沒考慮到使用的地方,先繪製放一邊吧)
主要的還是矩形,和圓形。不規則多邊形,實現較為複雜,尚未研究。但是我們可以通過矩形實現不規則多變型的範圍管理。
那我們開始繪製戰鬥區
指令碼CheckPoint
//此指令碼為小戰鬥區控制指令碼
//首先實現戰鬥區範圍控制,我們採用 Gizmos.DrawMesh() 的方式來繪製戰鬥區範圍
public enum CheckRange
{
Round, //圓形
Fan, //扇形
Rect //矩形
}
public class CheckPoint : MonoBehaviour {
//戰鬥區形狀
[Space(20)]
public CheckRange CheckRange = CheckRange.Round;
//圓形半徑
[Header("Round:")]
public float RoundRadio = 0;
//扇形引數
[Header("Fan")]
//角度
public float FanEnagle = 0;
//最小距離
[Range(0,1)]
public float FanMinRange = 0;
//最大劇情
public float FanMaxRange = 0;
//獲取最小距離
public float GetFanMinRange {
get {
return FanMaxRange * FanMinRange;
}
}
//矩形範圍
[Header("Rect")]
//長度
public float RectWidth = 0;
//寬度
public float RectHeight = 0;
//是否繪製Gizmos
public bool IsGzimos = false;
void OnDrawGizmos()
{
if (!IsGzimos) return;
DrawGizmos();
}
//繪製mesh
void DrawGizmos()
{
switch (CheckRange)
{
case CheckRange.Round:
Mesh round = MeshTools.MeshRound(RoundRadio,60);
Gizmos.color = new Color(1,0,0,0.5f);
Gizmos.DrawMesh(round,transform.position);
break;
case CheckRange.Fan:
Mesh fan = MeshTools.MeshFan(FanEnagle, FanMaxRange, GetFanMinRange);
Gizmos.color = new Color(1, 0, 0, 0.5f);
Gizmos.DrawMesh(fan, transform.position);
break;
case CheckRange.Rect:
Mesh rect = MeshTools.MeshRect(transform.position, RectWidth, RectHeight);
Gizmos.color = new Color(1, 0, 0, 0.5f);
Gizmos.DrawMesh(rect, transform.position);
break;
}
}
}
指令碼:MeshTools 此指令碼主要是繪製mesh
//此指令碼主要是繪製mesh
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class MeshTools {
/// <summary>
/// 圓形
/// </summary>
/// <param name="radio"></param>
/// <param name="pointAmount"></param>
/// <returns></returns>
public static Mesh MeshRound(float radio,int pointAmount)
{
float eachAngle = 360f / pointAmount;
List<Vector3> vertices = new List<Vector3>();
for (int i = 0; i <= pointAmount; i++)
{
Vector3 pos = Quaternion.Euler(0f, eachAngle * i, 0f) * Vector3.forward * radio;
vertices.Add(pos);
}
int[] triangles;
Mesh mesh = new Mesh();
int trangleAmount = vertices.Count - 2;
triangles = new int[3 * trangleAmount];
for (int i = 0; i < trangleAmount; i++)
{
triangles[3 * i] = 0;
triangles[3 * i + 1] = i + 1;
triangles[3 * i + 2] = i + 2;
}
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
/// <summary>
/// 矩形
/// </summary>
/// <param name="target"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public static Mesh MeshRect(Transform target,float width, float height)
{
List<Vector3> vertices = new List<Vector3>();
vertices.Add(new Vector3(0, 0, 0) - target.right * (width / 2));
vertices.Add(new Vector3(0, 0, 0) - target.right * (width / 2) + target.forward * height);
vertices.Add(new Vector3(0, 0, 0) + target.right * (width / 2) + target.forward * height);
vertices.Add(new Vector3(0, 0, 0) + target.right * (width / 2));
int[] triangles;
Mesh mesh = new Mesh();
int trangleAmount = vertices.Count - 2;
triangles = new int[3 * trangleAmount];
for (int i = 0; i < trangleAmount; i++)
{
triangles[3 * i] = 0;
triangles[3 * i + 1] = i + 1;
triangles[3 * i + 2] = i + 2;
}
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
/// <summary>
/// 矩形
/// </summary>
/// <param name="pos"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public static Mesh MeshRect(Vector3 pos, float width, float height)
{
List<Vector3> vertices = new List<Vector3>();
vertices.Add(new Vector3(width / 2, pos.y,height / 2));
vertices.Add(new Vector3( width / 2, pos.y, -height / 2));
vertices.Add(new Vector3(-width / 2, pos.y, -height / 2));
vertices.Add(new Vector3(-width / 2, pos.y, height / 2));
Mesh mesh = new Mesh();
int[] triangles = new int[3 * 2];
triangles[0] = 0;
triangles[1] = 1;
triangles[2] = 2;
triangles[3] = 0;
triangles[4] = 2;
triangles[5] = 3;
mesh.vertices = vertices.ToArray();
mesh.triangles = triangles;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
/// <summary>
/// 扇形
/// </summary>
/// <param name="angle"></param>
/// <param name="maxRadio"></param>
/// <param name="minRadio"></param>
/// <returns></returns>
public static Mesh MeshFan(float angle,float maxRadio,float minRadio)
{
//因為vertices(頂點)的個數與triangles(索引三角形頂點數)必須匹配
int vertices_count = 60 * 2 + 2;
Vector3[] vertices = new Vector3[vertices_count];
float angleRad = Mathf.Deg2Rad * angle;
float angleCur = angleRad;
float angledelta = angleRad / 60;
for (int i = 0; i < vertices_count; i += 2)
{
float cosA = Mathf.Cos(angleCur);
float sinA = Mathf.Sin(angleCur);
vertices[i] = new Vector3(maxRadio * cosA, 0, maxRadio * sinA);
vertices[i + 1] = new Vector3(minRadio * cosA, 0, minRadio * sinA);
angleCur -= angledelta;
}
//triangles:
int triangle_count = 60 * 6;
int[] triangles = new int[triangle_count];
for (int i = 0, vi = 0; i < triangle_count; i += 6, vi += 2)
{
triangles[i] = vi;
triangles[i + 1] = vi + 3;
triangles[i + 2] = vi + 1;
triangles[i + 3] = vi + 2;
triangles[i + 4] = vi + 3;
triangles[i + 5] = vi;
}
Mesh mesh = new Mesh();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
return mesh;
}
}
如此在Unity的Scene視窗中可以看見我們繪製的範圍了。
顯示效果:
好吧,範圍畫好了,我們接來需要考慮什麼呢?
戰鬥區,戰鬥區得需要有怪物吧,應該考慮投放怪物了,然而在一個戰鬥區中,往往會存在好幾波的怪物。這個要怎麼控制呢?
不然我們在戰鬥區得下面在建立一個指令碼 CheckPointWave,用來控制管理怪物或道具的投放。
那我們就需要考慮一下怪物的投放了
1.隨機投放(這個怎麼感覺用的應該都很少吧)
2.指定位置投放
還要考慮一下,我們怎麼控制著一波是否結束了呢?
我選擇最簡單的方式,那就沒死一隻怪物就會與當前投放的怪物數量進行比較,如果相等,那就代表著結束了。
指令碼:CheckPointWave 此指令碼為波數管理控制指令碼
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
public class CheckPointWave : MonoBehaviour {
/// <summary>
/// 波數
/// </summary>
public int Index = 0;
/// <summary>
/// 當波數結束時
/// </summary>
public Action OnWaveFinsh;
/// <summary>
/// 所屬戰鬥區
/// </summary>
[SerializeField]
private CheckPoint CheckPoint;
/// <summary>
/// 隨機建立
/// </summary>
public WaveRandomCreate RandomCreate;
/// <summary>
/// 指定位置建立
/// </summary>
public List<WavePosCreate> PosCreate = new List<WavePosCreate>();
/// <summary>
/// 下一波
/// </summary>
[SerializeField]
private CheckPointWave NextWave;
/// <summary>
/// 怪物列表
/// </summary>
private List<Actor> MonsterActor = new List<Actor>();
/// <summary>
/// 怪物數量
/// </summary>
public int MonsterNum { get { return RandomCreate.CreateNum + PosCreate.Count; } }
/// <summary>
/// 死亡數量
/// </summary>
public int DeathNum = 0;
/// <summary>
/// 開始事件
/// </summary>
public List<CpAction> OnStart = new List<CpAction>();
/// <summary>
/// 結束事件
/// </summary>
public List<CpAction> OnEnd = new List<CpAction>();
/// <summary>
/// 設定所屬戰鬥區
/// </summary>
/// <param name="cp"></param>
public void SetCheckPoint(CheckPoint cp)
{
CheckPoint = cp;
}
/// <summary>
/// 設定下一波
/// </summary>
/// <param name="nextWave"></param>
public void SetNextWave(CheckPointWave nextWave)
{
NextWave = nextWave;
}
/// <summary>
/// 當波數啟用
/// </summary>
public void OnActive()
{
//開始事件
if (OnStart.Count > 0)
{
for (int i = 0; i < OnStart.Count; i++)
CheckPoint.CpManager.DoCpAction(OnStart[i].ActionType, OnStart[i].ActionParam);
}
StartCoroutine(CreateActor());
//設定當前正在進行的是那一撥
CheckPoint.CpManager.SetCurWave(this);
}
/// <summary>
/// 建立怪物
/// </summary>
/// <returns></returns>
IEnumerator CreateActor()
{
if (RandomCreate.CreateNum != 0)
{
for (int i = 0; i < RandomCreate.CreateNum; i++)
{
yield return new WaitForSeconds(0.02f);
int index = UnityEngine.Random.Range(0, RandomCreate.MonsterID.Count);
CheckPoint.CpManager.CreateActor(RandomCreate.MonsterID[index], CheckPoint.GetRandomPos(), delegate(Actor actor) {
DeathNum++;
OnFinsh();
},delegate(Actor actor) {
MonsterActor.Add(actor);
});
}
}
if (PosCreate.Count > 0)
{
for (int i = 0; i < PosCreate.Count; i++)
{
yield return new WaitForSeconds(0.02f);
CheckPoint.CpManager.CreateActor(PosCreate[i].MonsterID, PosCreate[i].Postion, delegate(Actor actor) {
DeathNum++;
OnFinsh();
}, delegate (Actor actor) {
MonsterActor.Add(actor);
});
DeathNum++;
OnFinsh();
}
}
}
/// <summary>
/// 殺死所有怪物
/// </summary>
/// <param name="isPoint"></param>
public void kill(bool isPoint = false)
{
if (isPoint) NextWave = null;
for (int i = 0; i < MonsterActor.Count; i++)
{
if (!MonsterActor[i].ActorAttr.IsDeath)
MonsterActor[i].Kill();
}
}
/// <summary>
/// 拍段當前這波是否結束
/// </summary>
public void OnFinsh()
{
if (DeathNum != MonsterNum) return;
MonsterActor.Clear();
//結束事件
if (OnEnd.Count > 0)
{
for (int i = 0; i < OnEnd.Count; i++)
CheckPoint.CpManager.DoCpAction(OnEnd[i].ActionType,OnEnd[i].ActionParam);
}
//拍段是否進行下一波
if (NextWave != null)
{
NextWave.OnActive();
return;
}
//如果是最後一波控制當前戰鬥區結束
CheckPoint.OnFinsh();
}
}
/// <summary>
/// 隨機建立
/// </summary>
[System.Serializable]
public class WaveRandomCreate
{
public int CreateNum;
public float MinDis = 1;
public List<string> MonsterID = new List<string>();
}
/// <summary>
/// 固定位置建立
/// </summary>
[System.Serializable]
public class WavePosCreate
{
public string MonsterID;
public Vector3 Postion;
}
主要功能:
1.波數管理器啟用
/// <summary>
/// 當波數啟用
/// </summary>
public void OnActive()
{
//開始事件
if (OnStart.Count > 0)
{
for (int i = 0; i < OnStart.Count; i++)
CheckPoint.CpManager.DoCpAction(OnStart[i].ActionType, OnStart[i].ActionParam);
}
StartCoroutine(CreateActor());
//設定當前正在進行的是那一撥
CheckPoint.CpManager.SetCurWave(this);
}
2.建立怪物或道具
/// <summary>
/// 建立怪物
/// </summary>
/// <returns></returns>
IEnumerator CreateActor()
{
if (RandomCreate.CreateNum != 0)
{
for (int i = 0; i < RandomCreate.CreateNum; i++)
{
yield return new WaitForSeconds(0.02f);
int index = UnityEngine.Random.Range(0, RandomCreate.MonsterID.Count);
CheckPoint.CpManager.CreateActor(RandomCreate.MonsterID[index], CheckPoint.GetRandomPos(), delegate(Actor actor) {
DeathNum++;
OnFinsh();
},delegate(Actor actor) {
MonsterActor.Add(actor);
});
}
}
if (PosCreate.Count > 0)
{
for (int i = 0; i < PosCreate.Count; i++)
{
yield return new WaitForSeconds(0.02f);
CheckPoint.CpManager.CreateActor(PosCreate[i].MonsterID, PosCreate[i].Postion, delegate(Actor actor) {
DeathNum++;
OnFinsh();
}, delegate (Actor actor) {
MonsterActor.Add(actor);
});
DeathNum++;
OnFinsh();
}
}
}
3.結束,開始下一波,還是結束戰鬥區
/// <summary>
/// 拍段當前這波是否結束
/// </summary>
public void OnFinsh()
{
if (DeathNum != MonsterNum) return;
MonsterActor.Clear();
//結束事件
if (OnEnd.Count > 0)
{
for (int i = 0; i < OnEnd.Count; i++)
CheckPoint.CpManager.DoCpAction(OnEnd[i].ActionType,OnEnd[i].ActionParam);
}
//拍段是否進行下一波
if (NextWave != null)
{
NextWave.OnActive();
return;
}
//如果是最後一波控制當前戰鬥區結束
CheckPoint.OnFinsh();
}
恩恩,注意了,在程式碼中的Actor 類,為我想專案的演員物件,這個地方應該替換為你們自己演員物件
恩波數也有了,注意了,我們在程式碼中加入了兩個事件列表
/// <summary>
/// 開始事件
/// </summary>
public List<CpAction> OnStart = new List<CpAction>();
/// <summary>
/// 結束事件
/// </summary>
public List<CpAction> OnEnd = new List<CpAction>();
分別控制著當這波啟用觸發的事件和當這波結束後觸發的事件。這個咋們下面再說,你只要注意到了就好…
現在好了,戰鬥區有了,波數控制也有了。那接下來呢?
現在我們來補充一下控制區部分的程式碼,因為我們需要獲取到波數控制器啊,並且還要管理波數控制器的先後順序呢?在這裡我放出完整的CheckPoint 指令碼:
指令碼:CheckPoint
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
public enum CheckRange
{
Round,
Fan,
Rect
}
public class CheckPoint : MonoBehaviour {
/// <summary>
/// 戰鬥區ID
/// </summary>
public int Index = 0;
/// <summary>
/// 戰鬥區下一個ID
/// </summary>
public int NextIndex = -1;
/// <summary>
/// 是否啟用
/// </summary>
public bool IsActive = false;
/// <summary>
/// 是否檢測玩家
/// </summary>
public bool IsDetActor = false;
/// <summary>
/// 關卡管理器
/// </summary>
[SerializeField]
public CheckPointManager CpManager { get; private set; }
/// <summary>
/// 下一個戰鬥區
/// </summary>
[SerializeField]
private CheckPoint NextCheckPoint;
/// <summary>
/// 當前戰鬥區所有的波數管理器
/// </summary>
[SerializeField]
private List<CheckPointWave> Waves = new List<CheckPointWave>();
/// <summary>
/// 當戰鬥區結束事件
/// </summary>
public Action OnCheckFinsh;
[Header("當進入戰鬥開始時:")]
public List<CpAction> OnStartAction = new List<CpAction>();
[Header("當在戰鬥區時:")]
public List<CpAction> OnInsertAction = new List<CpAction>();
[Header("當戰鬥區結束時:")]
public List<CpAction> OnEndAction = new List<CpAction>();
//戰鬥區形狀
[Space(20)]
public CheckRange CheckRange = CheckRange.Round;
//圓形半徑
[Header("Round:")]
public float RoundRadio = 0;
//扇形引數
[Header("Fan")]
//角度
public float FanEnagle = 0;
//最小距離
[Range(0, 1)]
public float FanMinRange = 0;
//最大劇情
public float FanMaxRange = 0;
//獲取最小距離
public float GetFanMinRange
{
get
{
return FanMaxRange * FanMinRange;
}
}
//矩形範圍
[Header("Rect")]
//長度
public float RectWidth = 0;
//寬度
public float RectHeight = 0;
//是否繪製Gizmos
public bool IsGzimos = false;
/// <summary>
/// 設定關卡管理器
/// </summary>
/// <param name="cpManager"></param>
public void SetCPManager(CheckPointManager cpManager)
{
CpManager = cpManager;
}
/// <summary>
/// 設定下一個戰鬥區
/// </summary>
/// <param name="cp"></param>
public void SetNextCp(CheckPoint cp)
{
NextCheckPoint = cp;
}
void Start()
{
//獲取所有的波數管理器
CheckPointWave[] wave = transform.GetComponentsInChildren<CheckPointWave>();
for (int i = 0; i < wave.Length; i++)
{
wave[i].SetCheckPoint(this);
Waves.Add(wave[i]);
}
//根據波數管理器id進行排序
Waves.Sort((r1,r2)=> { if (r1.Index > r2.Index) return 1; else if (r1.Index == r2.Index) return 0;return -1; });
//設定波數
for (int i = 0; i < Waves.Count; i++)
{
if (i != Waves.Count - 1)
Waves[i].SetNextWave(Waves[i + 1]);
else
Waves[i].SetNextWave(null);
}
}
void Update()
{
if (CpManager == null) return;
if (Waves.Count < 1) return;
if (!IsDetActor) return;
///檢測玩家是否進入管戰鬥區
for (int i = 0; i < CpManager.DetActor.Count; i++)
{
if (DetActor(CpManager.DetActor[i]))
{
OnActive();
IsDetActor = false;
}
}
}
/// <summary>
/// 啟用戰鬥區
/// </summary>
public void OnActive()
{
IsActive = true;
///設定當前戰鬥區
CpManager.SetCurCheckPoint(this);
//第一波啟用
Waves[0].OnActive();
if (OnStartAction.Count > 0)
{
for (int i = 0; i < OnStartAction.Count; i++)
CpManager.DoCpAction(OnStartAction[i].ActionType, OnStartAction[i].ActionParam);
}
}
/// <summary>
/// 戰鬥區結束
/// </summary>
public void OnFinsh()
{
IsActive = false;
if (OnEndAction.Count > 0)
{
for (int i = 0; i < OnEndAction.Count; i++)
CpManager.DoCpAction(OnEndAction[i].ActionType,OnEndAction[i].ActionParam);
}
if (NextCheckPoint != null)
{
NextCheckPoint.IsDetActor = true;
return;
}
CpManager.OnFinsh();
}
/// <summary>
/// 檢測玩家是否進入戰鬥區
/// </summary>
/// <param name="actor"></param>
/// <returns></returns>
bool DetActor(Actor actor)
{
bool IsActive = false;
switch (CheckRange)
{
case CheckRange.Round:
IsActive = Vector3.Distance(actor.position,transform.position)<= RoundRadio;
break;
case CheckRange.Fan:
break;
case CheckRange.Rect:
Vector2 pos1 = new Vector2(transform.position.x+ RectWidth/2,transform.position.z+RectHeight/2);
Vector2 pos2 = new Vector2(transform.position.x - RectWidth / 2, transform.position.z - RectHeight / 2);
IsActive = actor.position.x <= pos1.x && actor.position.x >= pos2.x && actor.position.z <= pos1.y && actor.position.z >= pos2.y;
break;
}
return IsActive;
}
/// <summary>
/// 獲取隨機位置
/// </summary>
/// <returns></returns>
public Vector3 GetRandomPos()
{
Vector3 pos = Vector3.zero;
switch (CheckRange)
{
case CheckRange.Round:
Vector2 pos3 = new Vector2(transform.position.x + RoundRadio / 2, transform.position.z + RoundRadio / 2);
Vector2 pos4 = new Vector2(transform.position.x - RoundRadio / 2, transform.position.z - RoundRadio / 2);
float rx = UnityEngine.Random.Range(pos4.x, pos3.x);
float rz = UnityEngine.Random.Range(pos4.y, pos3.y);
Vector3 dir = new Vector3(rx, transform.position.y, rz);
float range = UnityEngine.Random.Range(0, RoundRadio);
pos = dir.normalized * range + transform.position;
break;
case CheckRange.Rect:
Vector2 pos1 = new Vector2(transform.position.x + RectWidth / 2, transform.position.z + RectHeight / 2);
Vector2 pos2 = new Vector2(transform.position.x - RectWidth / 2, transform.position.z - RectHeight / 2);
float x = UnityEngine.Random.Range(pos2.x, pos1.x);
float z = UnityEngine.Random.Range(pos2.y, pos1.y);
pos = new Vector3(x, 10, z);
break;
}
return pos;
}
void OnDrawGizmos()
{
if (!IsGzimos) return;
DrawGizmos();
}
/// <summary>
/// 繪製戰鬥區
/// </summary>
void DrawGizmos()
{
switch (CheckRange)
{
case CheckRange.Round:
Mesh round = MeshTools.MeshRound(RoundRadio,60);
Gizmos.color = new Color(1,0,0,0.5f);
Gizmos.DrawMesh(round,transform.position);
break;
case CheckRange.Fan:
Mesh fan = MeshTools.MeshFan(FanEnagle, FanMaxRange, GetFanMinRange);
Gizmos.color = new Color(1, 0, 0, 0.5f);
Gizmos.DrawMesh(fan, transform.position);
break;
case CheckRange.Rect:
Mesh rect = MeshTools.MeshRect(transform.position, RectWidth, RectHeight);
Gizmos.color = new Color(1, 0, 0, 0.5f);
Gizmos.DrawMesh(rect, transform.position);
break;
}
}
}
現在這個指令碼主要實現的功能有:
1.獲取當前戰鬥區下的所有波數管理器,並進行排序,控制波數的先後順序
void Start()
{
//獲取所有的波數管理器
CheckPointWave[] wave = transform.GetComponentsInChildren<CheckPointWave>();
for (int i = 0; i < wave.Length; i++)
{
wave[i].SetCheckPoint(this);
Waves.Add(wave[i]);
}
//根據波數管理器id進行排序
Waves.Sort((r1,r2)=> { if (r1.Index > r2.Index) return 1;