游戏要求
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求
- 必须使用订阅与发布模式传消息
- 使用工厂模式生产巡逻兵
设计过程
巡逻兵工厂
首先是巡逻兵的生产,与之前打飞碟游戏一样,设计一个类来专门负责生产巡逻兵,而在控制类中直接调用这个工厂即可。所以工厂也是需要单例化的,具体代码如下:
public class factory : MonoBehaviour {
private static factory _instance;
private List<GameObject> guard = new List<GameObject>();
private GameObject cur_guard;
public static factory getinstance()
{
if (_instance == null)
{
_instance = new factory();
}
return _instance;
}
public GameObject getguard(Vector3 po)
{
GameObject cur = Instantiate(Resources.Load<GameObject>("guard"), po, Quaternion.identity);
return cur;
}
}
巡逻兵移动脚本
对于巡逻兵来说,这里主要有两种运动模式
a. 自由巡逻 b. 追逐玩家
- 对于自由巡逻是通过行走固定路线来实现的,题目中要求的是行走一个多边形,我使用的是一个五边形路线。
五边形路线的生成方法是首先固定设置一块正方形区域,然后随机在四条边上选取四个点,再与左下角的点连接起来,形成一个多边形。
在创建巡逻兵的时候就生成这4个点,然后让巡逻兵依次顺着这5个点行走。(这里的x[order],y[order]为目标点的坐标,order为点的序号)
每次朝着某一个点行走时只需要在最开始设置方向即可,然后一直朝着那个点走,直到走到那个点附近。
void runtopoint(float px,float pz)
{
float lx = this.transform.position.x;
float lz = this.transform.position.z;
if (System.Math.Abs(lx - px) < delta && System.Math.Abs(lz - pz) < delta)
{
order++;
order %= 5;
//Debug.Log("change dir");
dir = new Vector3(x[order] - lx, 0, z[order] - lz);
this.transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
}
else
{
//Debug.Log("run");
this.transform.position += dir * Time.fixedDeltaTime * 0.5f;
}
}
- 对于追逐玩家时首先需要得到玩家的坐标,然后设置方向,朝着玩家前进,直到接近玩家或者玩家逃出范围,在追逐失败之后巡逻兵继续进行自由巡逻状态,朝着追逐玩家之前朝向的目标点前进。
首先是获取玩家坐标,这里使用private void OnTriggerEnter(Collider other)
函数来进行判断,当有物体触碰到巡逻兵的Box Collider时就会触发这个函数,然后在这个函数里面判断触碰到的对象是否是玩家,这里通过tag来判断。
private void OnTriggerEnter(Collider other)
{
if (other.tag == "Player")
{
//Debug.Log("run to player");
mod = 2;
}
}
为了达到此目的,需要对巡逻兵添加Box Collider组件
(这里的size即为“视野”大小)
- 对于巡逻兵的动画设置
由于巡逻兵只有行走这个动画,没有添加攻击,所以animator控制就非常的简单
- 对玩家的坐标获取
玩家首先会触发OnTriggerEnter函数表示已经进入巡逻兵视野,然后这时只要玩家一直在视野,就会不断触发OnTriggerStay函数,在这个函数里就可以获取玩家的坐标,以及设置巡逻兵的方向。
private void OnTriggerStay(Collider other)
{
if (other.tag == "Player")
{
if (t < 1f)
{
t += Time.deltaTime;
}
else
{
playerx = other.transform.position.x;
playerz = other.transform.position.z;
setdir(playerx, playerz);
t = 0f;
}
}
}
(这里并没有实时的获取坐标,而是隔一段时间获取是为了减少计算量。)
5. 恢复巡逻状态
使用OnTriggerExit函数来执行,当玩家离开视野后调用,设置下一个目标点。
private void OnTriggerExit(Collider other)
{
if (other.tag == "Player")
{
//Debug.Log("out of player");
mod = 1;
setdir(x[order], z[order]);
rule.getinstance().addscore();
}
}
玩家控制
玩家的模型我是在unity的商店下载的一个,使用到的动画只有跑步,而且其自带了控制脚本,所以可以直接使用键盘对其进行操作。代码也不在这里展示了。
订阅与发布者模式
这个模式主要功能是将想订阅的时间注册到调度中心,然后事件触发的时候发布者发布该事件到调度中心,然后由调度中心调度订阅者的处理代码。
实现的方法是使用一个规则类来对分数进行记录,当巡逻兵判断玩家走出视野后就调用规则类使其记录的分数加一从而实现。这个规则类也需要单例化。
public class rule {
int score;
private static rule _instance;
public static rule getinstance()
{
if (_instance == null)
{
_instance = new rule();
}
return _instance;
}
void Start()
{
init();
}
public void init()
{
score = 0;
}
public void addscore()
{
score++;
}
public int getscore()
{
return score;
}
}
游戏的控制
对于整个游戏的进程,使用control类来进行控制,在游戏开始时初始化各种参数变量。
- UI界面
对于界面的显示主要有一个按钮与一个文本框,按钮触发游戏开始,文本框显示游戏分数以及游戏的一些相关信息。这里需要注意的是位置的设置,使用screen.width来获取屏幕的宽度,从而设置其居中显示。然后如果需要对其位置大小等进行改变,只需要在设置它的时候使用变量实时改变即可,界面如下:
实现的代码如下:
private void OnGUI()
{
if (GUI.Button(new Rect(bux,buy,buwidth,buheight), "begin"))
{
director.getinstance().setbegin();
bux = 0;
buy = 0;
buheight = 0;
buwidth = 0;
//director.getinstance().setrun();
//Debug.Log(director.getinstance().getstate());
}
GUI.color = Color.black;
GUI.Label(new Rect(labelx, labely, labelwid, labelhe), showlabel);
}
(这里设置按钮在点击之后移到左上角隐藏)
- 游戏的进程
对于游戏的控制开始与结束使用一个导演类来进行,当开始按钮点击时设置导演类的参数为开始,从而通知各个对象或者函数游戏开始,这里主要是control中update的设置。在处理这个的时候我使用了预备和进行两个状态,点击按钮之后进入准备状态,初始化巡逻兵和玩家,然后才设置为运行状态,游戏开始运行。
void Update () {
if (director.getinstance().getstate() == 1)
{
//Debug.Log("create");
guard[0] = factory.getinstance().getguard(new Vector3(2.5f, 0f, 2.5f));
guard[1] = factory.getinstance().getguard(new Vector3(-22.5f, 0f, -22.5f));
guard[2] = factory.getinstance().getguard(new Vector3(-22.5f, 0f, 2.5f));
director.getinstance().setrun();
player.SetActive(true);
}
else if (director.getinstance().isover() == true)
{
player.SetActive(false);
labelx = Screen.width / 2 - labelx / 2;
labely = Screen.height / 2;
showlabel = "you lose and the score is " + rule.getinstance().getscore();
}
else
{
showlabel = "score: " + rule.getinstance().getscore();
}
}
游戏的结束在巡逻兵里面设置,一旦触发结束条件就通知导演游戏结束,并使控制类进行相关显示。
对于导演类同样需要单例化:
public class director {
private static director _instance;
public int gamestate;//0 for stop 1 for ready 2 for run 3 for over
public void init()
{
gamestate = 0;
}
public void setbegin()
{
gamestate = 1;
}
public void setrun()
{
gamestate = 2;
}
public void setover()
{
gamestate = 3;
}
public bool isover()
{
if (gamestate == 3)
{
return true;
}
return false;
}
public int getstate()
{
return gamestate;
}
public static director getinstance()
{
if (_instance == null)
{
_instance = new director();
}
return _instance;
}
}
- 镜头的控制
采用的是镜头一直追踪玩家后背的形式,拷贝的网上的代码,但也挺简单的:
public class cameracontrol : MonoBehaviour
{
public Transform target=null;
public float distanceUp = 5f;
public float distanceAway = 2f;
public float smooth = 60f;//位置平滑移动值
public float camDepthSmooth = 5f;
//public bool begin = false;
bool setalready;
// Use this for initialization
void Start()
{
setalready = false;
//target = GameObject.FindGameObjectWithTag("Player").transform;
}
// Update is called once per frame
void Update()
{
if (director.getinstance().getstate()==2 && setalready == false)
{
target = GameObject.FindGameObjectWithTag("Player").transform;
setalready = true;
}
// 鼠标轴控制相机的远近
if ((Input.mouseScrollDelta.y < 0 && Camera.main.fieldOfView >= 3) || Input.mouseScrollDelta.y > 0 && Camera.main.fieldOfView <= 80)
{
Camera.main.fieldOfView += Input.mouseScrollDelta.y * camDepthSmooth * Time.deltaTime;
}
//}
}
void LateUpdate()
{
if (director.getinstance().getstate() == 2 && setalready == false)
{
target = GameObject.FindGameObjectWithTag("Player").transform;
setalready = true;
}
//相机的位置
Vector3 disPos = target.position + Vector3.up * distanceUp - target.forward * distanceAway;
transform.position = Vector3.Lerp(transform.position, disPos, Time.deltaTime * smooth);
//相机的角度
transform.LookAt(target.position);
}
}
游戏最终运行截图
项目地址