前言
这次的作业是在智能巡逻兵的基础上,改的一个新游戏。本来第七次作业想要效仿学长的3d大作,没想到各种fsm,欧拉角的应用之类的看得我发蒙;之后照着抄也是抄出一堆bug,不得已只好随便敷衍一下,做个简单的游戏。
现在有时间来做后,大作业依旧抄不明白在学长的框架上进行了规则,应用的修改,和视野可视化的添加。
但比较遗憾的是,没有自动寻路,地图不敢设置得太复杂,由衷希望后来的学弟能在这上面补齐功能,实现完整的追逐逃生游戏。
一、游戏规则
玩家需要躲避巡逻兵的追捕,集齐环境中的所有钥匙,最终逃生。
巡逻兵会沿一定轨迹行动,追击视野中的玩家。
玩家一旦被追上,立即死亡。
二、具体实现
由于本次框架基本与学长的一致,部分细节不在一一展示。
1.人物
动画器
动画器里,我删掉了后跳,额外增加了拾取,攻击与死亡的动画。这里本来还想加一个守卫原地警戒的动作,但找不到好的动画,只好不了了之。具体细节在学长博客可以了解,我就不班门弄斧了。
玩家模型
守卫模型(视野可视化详解)
视野感知的原理
首先,我先介绍一下视野感知的原理:(参考)
第一步,我们开启圆形的碰撞器(Sphere Collider),这可以理解为敌人的感知范围
第二步,一旦有玩家进入这个范围,便计算正前方与玩家区域的角度差,即视角
第三步,从敌人所在部分发射一条射线,射线如果可以不受障碍物影响击中敌人,即可见
感知范围间,一定视角内的可见玩家,就可以被敌人感知,如图。
模型解析
然后我们来拆解一下这个守卫模型
大的父对象Guard,搭载守卫的数据,动画,碰撞体等
ybot是直接套的人物模型,什么都不加,而且这里不搭载动画,因为父对象已经搭载了。
然后是view,需要额外添加碰撞器与视野的脚本。
最后是我们可视化的视野,Spot light: 聚光灯,类似手电筒(参考:
光源组件1、光源组件2)。
这里面主要注意以下属性:
Color 光的颜色。
Range 范围,控制点光源和聚光灯照射的范围距离,平行光没有这个属性。
Spot Angle 角度,控制聚光灯圆锥张角的大小,只有聚光灯有这个属性。
Intensity 强度 ,控制光照的强度。
代码解析(GuardView)
所用属性
GameObject Target = null;
public bool isInView = false;//是否在扇形视野范围内
public bool isRayCast = false;//是否通过射线能看到
public bool isAlert = false;//是否警报
float PerceiveRadius = 8f;//感知范围
// float AlertTime = 10f;//警报时间
// float WaitTime = 4f;//失去目标等待时间
float LightRange = 0f;//灯光距离(在Start()中根据SpotLight参数设定)
float LightIntensity = 0f;//灯光亮度(在Start()中根据SpotLight参数设定)
float ViewAngle = 60f;//视野角度
float TargetAngle = 0;
float AngleDiff = 0;
计算角度差
void CalculateAngle()
{
if (Target != null)
{
float AtanAngle = (Mathf.Atan((Target.transform.position.z - this.transform.position.z) /
(Target.transform.position.x - this.transform.position.x))
* 180.0f / 3.14159f);
//Debug.Log (this.transform.rotation.eulerAngles+" "+AtanAngle);
//1象限角度转换
if ((Target.transform.position.z - this.transform.position.z) > 0
&&
(Target.transform.position.x - this.transform.position.x) > 0
)
{
TargetAngle = 90f - AtanAngle;
//Debug.Log ("象限1 "+TargetAngle);
}
//2象限角度转换
if ((Target.transform.position.z - this.transform.position.z) <= 0
&&
(Target.transform.position.x - this.transform.position.x) > 0
)
{
TargetAngle = 90f + -AtanAngle;
//Debug.Log ("象限2 "+TargetAngle);
}
//3象限角度转换
if ((Target.transform.position.z - this.transform.position.z) <= 0
&&
(Target.transform.position.x - this.transform.position.x) <= 0
)
{
TargetAngle = 90f - AtanAngle + 180f;
//Debug.Log ("象限3 "+TargetAngle);
}
//4象限角度转换
if ((Target.transform.position.z - this.transform.position.z) > 0
&&
(Target.transform.position.x - this.transform.position.x) <= 0
)
{
TargetAngle = 270f + -AtanAngle;
//Debug.Log ("象限4 "+TargetAngle);
}
//调整TargetAngle
float OriginTargetAngle = TargetAngle;
if (Mathf.Abs(TargetAngle + 360 - this.transform.rotation.eulerAngles.y)
<
Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y)
)
{
TargetAngle += 360f;
}
if (Mathf.Abs(TargetAngle - 360 - this.transform.rotation.eulerAngles.y)
<
Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y)
)
{
TargetAngle -= 360f;
}
//输出角度差
AngleDiff = Mathf.Abs(TargetAngle - this.transform.rotation.eulerAngles.y);
// Debug.Log("角度差:" + TargetAngle + "(" + OriginTargetAngle + ")-" + this.transform.rotation.eulerAngles.y + "=" + AngleDiff);
}
}
感知视野的相关计算 判断isRayCast和isInView
void JudgeView()
{
//感知角度相关计算
if (Target != null)
{
Debug.Log(Target);
//指向玩家的向量计算
Vector3 vec = new Vector3(Target.transform.position.x - this.transform.position.x,
0f,
Target.transform.position.z - this.transform.position.z);
Debug.Log(this.transform.position);
Debug.Log(Target.transform.position);
Debug.Log(vec);
//射线碰撞判断
RaycastHit hitInfo;
if (Physics.Raycast(this.transform.position, vec, out
hitInfo,20,LayerMask.GetMask("Player")|LayerMask.GetMask("Ground")))
{
GameObject gameObj = hitInfo.collider.gameObject;
//Debug.Log("Object name is " + gameObj.name);
if (gameObj.tag == "Player")//当射线碰撞目标为boot类型的物品 ,执行拾取操作
{
Debug.Log("Seen!");
isRayCast = true;
}
else
{
isRayCast = false;
}
}
//画出碰撞线
Debug.DrawLine(this.transform.position, hitInfo.point, Color.red, 1);
//视野中的射线碰撞判断结束
//视野范围判断
//物体在范围角度内,警戒模式下范围为原来1.5倍
if (AngleDiff * 2 <
(isAlert ? ViewAngle * 1.5f : ViewAngle)
)
{
isInView = true;
}
else
{
isInView = false;
}
// Debug.Log ("角度差 "+AngleDiff);
}
}
更新状态
void Update()
{
//Debug.Log("state:" + state + " time_alert:" + time_alert);
if (isAlert)
{
//警戒模式
m_Light.GetComponent<Light>().range = LightRange * 2f;
m_Light.GetComponent<Light>().color = new Color(0.784f, 0.317f, 0.203f,1f);
m_Light.GetComponent<Light>().intensity = LightIntensity * 2f;
m_Light.GetComponent<Light>().spotAngle = ViewAngle * 1.5f;
this.GetComponent<SphereCollider>().radius = PerceiveRadius * 1.5f;
}
else
{
//正常模式
m_Light.GetComponent<Light>().range = LightRange;
m_Light.GetComponent<Light>().color = new Color(0.257f, 0.745f, 0.108f,1f);
m_Light.GetComponent<Light>().intensity = LightIntensity;
m_Light.GetComponent<Light>().spotAngle = ViewAngle;
this.GetComponent<SphereCollider>().radius = PerceiveRadius;
}
//计算角度差
CalculateAngle();
//感知视野判断(判断isRayCast与isInView)
JudgeView();
}
进行感知
//玩家进入感知层
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Player")
{
Target = other.gameObject;
//提前计算角度差
CalculateAngle();
}
}
//玩家进入视野
void OnTriggerStay(Collider other)
{
if (other.gameObject.tag == "Player")
{
if (Target == null)
{
Target = other.gameObject;
}
}
}
//玩家离开感知层
void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "Player")
{
Target = null;
isInView = false;
isRayCast = false;
}
}
注意事项
实际上在运行游戏时,很容易发送碰撞事故,如:玩家在守卫身体上跳跃,守卫感知不到地面,探测射线卡在守卫体内等。这里就需要对Layer(层layer) 、LayerMask (遮罩层)进行修改(参考:层、特定碰撞),实现特定碰撞。一般有两种方式,第一种是直接在设置中声明:
另一种是在代码中声明:
我的层次是这样的
这里拿射线探测举例,只探测玩家与墙壁即可:
RaycastHit hitInfo;
if (Physics.Raycast(this.transform.position, vec, out
hitInfo,20,LayerMask.GetMask("Player")|LayerMask.GetMask("Ground")))
2.场景
守卫是沿矩形运动的,图也设置得比较简单。
其中,我除了学长使用的区域触发器,我额外加了门与钥匙的探测器和代码
3.控制器
常见的mvc模板就不再放出,这里展示一下总控制器和事务控制器。
FirstSceneController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController {
public GuardFactory guard_factory; //巡逻者工厂
public GuardActionManager action_manager; //运动管理器
public int playerSign = -1; //当前玩家所处哪个格子
public GameObject player; //玩家
public UserGUI gui; //交互界面
private List<GameObject> guards; //场景中巡逻者列表
private int game_status = 0; //游戏状态 0:游戏中 ; 1:游戏失败; 2:游戏胜利
public int keynum = 0; //钥匙数量
public bool tryget = false; //正在尝试获得钥匙
void Awake() {
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
guard_factory = Singleton<GuardFactory>.Instance;
action_manager = gameObject.AddComponent<GuardActionManager>() as GuardActionManager;
gui = gameObject.AddComponent<UserGUI>() as UserGUI;
LoadResources();
}
void Update() {
for (int i = 0; i < guards.Count; i++) {
guards[i].gameObject.GetComponent<GuardData>().playerSign = playerSign;
}
}
public void LoadResources() {
Instantiate(Resources.Load<GameObject>("Prefabs/Road"));
player = Instantiate(
Resources.Load("Prefabs/Player"),
new Vector3(-5, 8, 28), Quaternion.identity) as GameObject;
guards = guard_factory.GetPatrols();
for (int i = 0; i < guards.Count; i++) {
action_manager.GuardPatrol(guards[i]);
}
}
public int GetGamestatus() {
return game_status;
}
public void Restart() {
SceneManager.LoadScene("Scenes/mySence");
}
void OnEnable() {
GameEventManager.ExitChange += PlayerExit;
GameEventManager.GameoverChange += Gameover;
GameEventManager.GetKeyChange += getkey;
GameEventManager.GetKeyingChange += getkeying;
}
void OnDisable() {
GameEventManager.ExitChange -= PlayerExit;
GameEventManager.GameoverChange -= Gameover;
GameEventManager.GetKeyChange -= getkey;
GameEventManager.GetKeyingChange -= getkeying;
}
//游戏失败
void Gameover() {
game_status = 1;
}
//逃生
void PlayerExit() {
if(keynum == 2)game_status = 2;
}
//获得钥匙
bool getkey(){
if(tryget){
keynum++;
tryget = false;
return true;
}
return false;
}
//玩家尝试拾取
void getkeying(bool trying){
tryget = trying;
}
}
GameEventManager
public class GameEventManager : MonoBehaviour {
public delegate void ExitEvent();
public static event ExitEvent ExitChange;
public delegate void GameoverEvent();
public static event GameoverEvent GameoverChange;
public delegate void GetKeyingEvent(bool trying);
public static event GetKeyingEvent GetKeyingChange;
public delegate bool GetKeyEvent();
public static event GetKeyEvent GetKeyChange;
public void PlayerExit() {
if (ExitChange != null) {
ExitChange();
}
}
public void PlayerGameover(){
if (GameoverChange != null) {
GameoverChange();
}
}
public void PlayerGetkeying(bool trying){
if (GetKeyingChange != null) {
GetKeyingChange(trying);
}
}
public bool PlayerGetkey(){
if (GetKeyChange != null) {
return GetKeyChange();
}
return false;
}
}
三、视频与效果图
逃生
四、总结
这位学长的基础真的很扎实,框架简洁干净又优雅,在学他框架的同时,我几乎把本学期前面学的东西全都复习了一次。最后,虽然大部分我是在学长的基础里修改的,但我也粗略地学会了动画的使用,也对游戏做出了一定的改进,对这次作业,本人还是很知足的。