模型与动画

模型与动画

PS:所有代码请点击下方代码传送门,最终游戏动画也在里面
代码传送门
对了,里面的 Warning 是我在 store 里找的资源的脚本中的代码用的函数太老旧了,我懒得改了。。



智能巡逻兵

游戏设计要求:

  • 创建一个地图和若干巡逻兵(使用动画);
  • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
  • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
  • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
  • 失去玩家目标后,继续巡逻;
  • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;

程序设计要求:

  • 必须使用订阅与发布模式传消息
  • 工厂模式生产巡逻兵

题目意思

  害,就是审题嘛。这次的要求中,比较重要的几个如下:

  • 碰撞障碍物换目标点
  • 感知到玩家就追击
  • 失去玩家目标后继续巡逻
  • 使用订阅与发布模式

  只不过呢,这个游戏我不太想做的火药味儿太重,所以我其实是按一个抓人小游戏的氛围来做的,快乐重要嘛。所以,我找的玩家模型在被抓到之后是摆个 pose:伸懒腰。然后巡逻兵呢,也是只阔爱的柴犬。可惜就是这柴犬竟然没有拥抱啥的 pose,我佛了。但迫于柴奴本能,我还是用他了。。

Resources

  这次我采用的模型有下列这些:

  • Anime Character: Arisa
  • Dog Knight PBR Polyart
  • StoneWalls Normal Maps

  上述资源中,Arisa 是玩家模型,还挺漂亮咧(lsp警告);Dog Knight 是个柴犬骑士,太nm阔爱了。最后就是一个石墙。前俩模型如下:
Arisa
DogKnight
迷宫模型:
Map
  先提提柴犬。柴犬上我加了一个 Sphere Collider,用来检测与玩家的碰撞以及玩家是否在范围内或者脱离。然后是map。有一说一,这Map的设计需要的东西有点多,首先是地板,然后是墙,最后是每块区域(区域编号如图)还要搞个 Box Collider 去感应角色进入。。。这里,我发现用空对象,放个 Box Collider 进去,好像没啥用。最后我就还是用了 cude 对象,然后这些 cube 对象藏墙里了。。。我觉得我这波操作很妙,哈哈哈,打扰了
  咱继续说模型。Arisa 有跑动、退步走和伸懒腰的pose。狗骑士有走、跑还有攻击的动作。部分(Arisa)图如下:
run
walk
pose
接下来是 animator。Player 与 Pratrol 的 animator 分别如下:
Player Animator
Player
AIPatrol Animator
Patrol
  大概就这么多咯。具体就到项目那儿看了呗。

程序设计

  这次,由于动作简单,之间也没有什么关联,所以就没有再去做动作管理器。废话不多说,直接开搞。

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

实验结果

  说了这么多,上个游戏过程瞅瞅咯。
游戏开始:
GameStart
游戏中:
Playing
游戏结束:
GameOver
游戏动画:
  gif 应该是太大了,拉不上来,所以得到 gitee 去看了:
游戏动画

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值