模型与动画
PS:所有代码请点击下方代码传送门,最终游戏动画也在里面
代码传送门
对了,里面的 Warning 是我在 store 里找的资源的脚本中的代码用的函数太老旧了,我懒得改了。。
目录
智能巡逻兵
游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求:
- 必须使用订阅与发布模式传消息
- 工厂模式生产巡逻兵
题目意思
害,就是审题嘛。这次的要求中,比较重要的几个如下:
- 碰撞障碍物换目标点
- 感知到玩家就追击
- 失去玩家目标后继续巡逻
- 使用订阅与发布模式
只不过呢,这个游戏我不太想做的火药味儿太重,所以我其实是按一个抓人小游戏的氛围来做的,快乐重要嘛。所以,我找的玩家模型在被抓到之后是摆个 pose:伸懒腰。然后巡逻兵呢,也是只阔爱的柴犬。可惜就是这柴犬竟然没有拥抱啥的 pose,我佛了。但迫于柴奴本能,我还是用他了。。
Resources
这次我采用的模型有下列这些:
- Anime Character: Arisa
- Dog Knight PBR Polyart
- StoneWalls Normal Maps
上述资源中,Arisa 是玩家模型,还挺漂亮咧(lsp警告);Dog Knight 是个柴犬骑士,太nm阔爱了。最后就是一个石墙。前俩模型如下:
迷宫模型:
先提提柴犬。柴犬上我加了一个 Sphere Collider,用来检测与玩家的碰撞以及玩家是否在范围内或者脱离。然后是map。有一说一,这Map的设计需要的东西有点多,首先是地板,然后是墙,最后是每块区域(区域编号如图)还要搞个 Box Collider 去感应角色进入。。。这里,我发现用空对象,放个 Box Collider 进去,好像没啥用。最后我就还是用了 cude 对象,然后这些 cube 对象藏墙里了。。。我觉得我这波操作很妙,哈哈哈,打扰了
咱继续说模型。Arisa 有跑动、退步走和伸懒腰的pose。狗骑士有走、跑还有攻击的动作。部分(Arisa)图如下:
接下来是 animator。Player 与 Pratrol 的 animator 分别如下:
Player Animator
AIPatrol Animator
大概就这么多咯。具体就到项目那儿看了呗。
程序设计
这次,由于动作简单,之间也没有什么关联,所以就没有再去做动作管理器。废话不多说,直接开搞。
SSDirector、ScoreRecorder、Singleton、UserGUI
这些咱就不说了,东西就那些,没啥变动。
ISceneController
- LoadResources():加载资源
- IsGameStart():用于判断游戏是否开始
- IsGameOver():用于判断游戏是否结束
IUserAction
- GetScore():获取得分
- UAIsGameOver():判断游戏是否结束,与 ISceneController 中的函数函数名上作了区分
- GameStart():游戏开始
- Restart():重新开始
PlayerData
PlayerData 其实就一样东西:player_sign,用来标记 Player 当前处在哪一块区域。
[System.Serializable]
public class PlayerData : MonoBehaviour{
[Tooltip("所在区域")]
public int player_sign = 0;
}
Player
Player 的在移动控制上(FixedUpdate 函数)的编写参考了 Arisa 资源中自带的脚本。
public class Player : MonoBehaviour
{
public float forwardSpeed = 7.0f;
public float backwardSpeed = 3.0f;
public float rotateSpeed = 3.0f;
private Vector3 velocity;
private AnimatorStateInfo currentBaseState;
private Animator anim;
private Rigidbody rb;
// Start is called before the first frame update
void Start()
{
anim = GetComponent<Animator> ();
rb = GetComponent<Rigidbody> ();
}
// Update is called once per frame
void FixedUpdate()
{
SSDirector director = SSDirector.GetInstance();
// Debug.Log(director.CurrentScenceController.isGameOver());
if(director.CurrentScenceController.IsGameOver() || !director.CurrentScenceController.IsGameStart()){
return;
}
float h = Input.GetAxis ("Horizontal");
float v = Input.GetAxis ("Vertical");
if(h!=0 || v!=0){
anim.SetBool("isMoving",true);
}
else{
anim.SetBool("isMoving",false);
}
anim.SetFloat("direction",v);
// Debug.Log("v: " + v);
velocity = new Vector3 (0, 0, v);
velocity = transform.TransformDirection (velocity);
if (v > 0.1) {
velocity *= forwardSpeed;
} else if (v < -0.1) {
velocity *= backwardSpeed;
}
rb.velocity = velocity;
transform.Rotate (0, h * rotateSpeed, 0);
}
}
Patrol
Patrol 脚本比较重要的函数说明如下:
- Update():函数其实完成的就是移动的工作。其中有两种移动方式,Walk 的移动方式需要由 direction 来控制方向。Run 的方向则由 Arisa 与 柴犬 间的向量来确定。这里,我使用了 OnTriggerStay 来获得当 Arisa 在侦查范围内时的位置。
- OnTriggerEnter():检测玩家进入范围,开始追击
- OnTriggerStay():检测玩家在范围内时的位置
- OnTriggerExit():判断玩家脱离
- OnCollisionEnter():当与玩家发生碰撞,即抓到玩家,游戏结束
具体看项目吧,有点太长了,一百多行。。。
PatrolFactory
- GetPatrol:创建柴犬骑士
- StopPatrol:让柴犬骑士停止,罚站
- Reset:重置所有柴犬骑士
public class PatrolFactory : MonoBehaviour
{
public GameObject patrol_prefab = null; // 用于保存GetDisk结果并返回
private List<GameObject> patrols = new List<GameObject>();
public GameObject GetPatrol(int area_num){
patrol_prefab = null;
patrol_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));
patrols.Add(patrol_prefab);
switch (area_num)
{
case 1: patrol_prefab.transform.position = new Vector3(0,0,-10);break;
case 2: patrol_prefab.transform.position = new Vector3(-20,0,-10);break;
case 3: patrol_prefab.transform.position = new Vector3(20,0,10);break;
case 4: patrol_prefab.transform.position = new Vector3(0,0,10);break;
case 5: patrol_prefab.transform.position = new Vector3(-20,0,10);break;
}
return patrol_prefab;
}
public void StopPatrol(){
for(int i=0;i<patrols.Count;i++){
patrols[i].GetComponent<Animator>().SetBool("gameOver",true);
patrols[i].GetComponent<Rigidbody>().isKinematic = true;
}
}
public void Reset(){
for(int i=0;i<patrols.Count;i++){
patrols[i].GetComponent<Animator>().SetBool("gameOver",false);
patrols[i].GetComponent<Rigidbody>().isKinematic = false;
switch(i){
case 0: patrols[i].transform.position = new Vector3(0,0,-10);break;
case 1: patrols[i].transform.position = new Vector3(-20,0,-10);break;
case 2: patrols[i].transform.position = new Vector3(20,0,10);break;
case 3: patrols[i].transform.position = new Vector3(0,0,10);break;
case 4: patrols[i].transform.position = new Vector3(-20,0,10);break;
}
}
}
}
GameEventManager
此次项目的一个重点来了。我觉得老师的以及网上很多一些博客的解释都很清晰了,但我还是再重复一次咯,等巨佬纠纠错。
其实,就是用了 delegate。在一个类里面声明好 delegate 之后,发布者就只需要在遇到某些事情时触发一下(也就是调用呗)类里的函数。然后接受者就只需要将自己的一个函数挂载在某个 delegate 上就行。下面用个可能能算伪码的东西展示下流程咯,应该还算清晰了。。
- delegate类:IAmAngry(){call delegate1();}
- 发布者:if(我生气了){ call IAmAngry(); }
- 接收者:(IGetThatYouAreAngry 是一个实现在接收者中的函数,+= 时只要加函数名,类似于传了个函数指针)
- 想要接收时:delegate1 += IGetThatYouAreAngry;
- 如果不想接收了:delegate1 -= IGetThatYouAreAngry;
public class GameEventManager : MonoBehaviour
{
public delegate void ScoreEvent();
public static event ScoreEvent ScoreChange;
public delegate void GameoverEvent();
public static event GameoverEvent GameoverChange;
public delegate void CrystalEvent();
public static event CrystalEvent CrystalChange;
public void AddScore()
{
if (ScoreChange != null)
{
ScoreChange();
}
}
public void GameOver()
{
if (GameoverChange != null)
{
GameoverChange();
}
}
public void ReduceCrystalNum()
{
if (CrystalChange != null)
{
CrystalChange();
}
}
}
FirstController
FirstController 主要就是加载资源还有实现接口咯。具体看代码吧,堆起来还是有点多的。
AreaCollider
就是挂载在 Map 模型中那六个 Cube 上的,用来判断 Player 是否进入了去榆中,然后修改 Player 的当前所在区域 player_sign。
public class AreaCollider : MonoBehaviour
{
public int sign;
FirstController sceneController;
void Start()
{
sceneController = SSDirector.GetInstance().CurrentScenceController as FirstController;
sign = gameObject.name[gameObject.name.Length-1] - '0';
}
void OnTriggerEnter(Collider c)
{
//标记玩家进入自己的区域
if (c.gameObject.tag == "Player")
{
c.gameObject.GetComponent<PlayerData>().player_sign = sign;
}
}
}
实验结果
说了这么多,上个游戏过程瞅瞅咯。
游戏开始:
游戏中:
游戏结束:
游戏动画:
gif 应该是太大了,拉不上来,所以得到 gitee 去看了:
游戏动画