欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

3D游戏(7)——模型与动画

程序员文章站 2022-07-12 23:31:09
...

1、智能巡逻兵

游戏设计要求

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

程序设计要求

  • 必须使用订阅与发布模式传消息
    • subject:OnLostGoal
    • Publisher: ?
    • Subscriber: ?
  • 工厂模式生产巡逻兵

友善提示1:生成 3~5个边的凸多边型

  • 随机生成矩形
  • 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
  • 5 ?

友善提示2:参考以前博客,给出自己新玩法

游戏程序设计

首先,在找素材的时候找到一个别具一格的素材。Scivolo Character Controller

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画
3D游戏(7)——模型与动画

素材包里面不仅有一个有趣的地形(可供巡逻兵游戏的进行),还实现了第三人称的玩家模型,不仅可以通过鼠标来控制视图,可以上下左右走动,还可以跳,爬楼梯的更是不在话下。

因而,地图稍微改改布局,改宽改大,就直接用作本次游戏的场景了。人物的话计划也是改改模型即可,脚本还是用原来的。怎一个舒服了得

3D游戏(7)——模型与动画

稍微将原来的地图改改,适当加几个AreaCollider,用于后续触发巡逻兵的追捕,如此一来这个游戏的地图就完成了。

3D游戏(7)——模型与动画

需要注意的是AreaCollider里面的碰撞体需要设置isTrigger。

可惜的是,如此好的一个地图却无法做成预制,或许是因为这个资源包里面的Mesh Filter缺少的缘故,做成预制之后再拖出来就复原不了现在这个样子了。

3D游戏(7)——模型与动画

原因就是做成预制之后上图的Mesh就变成了空。

这么一来的话,做不成预制,就无法像以往那样在运行时再实例化地图了,只能够通过修改Active这种蠢蠢的方式了。手动狗头

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画

人物模型的话呢,我也找到了一个不错的,VoxelCharacters。在这里我们用到三种人物预制。

3D游戏(7)——模型与动画

Hero用作玩家。

3D游戏(7)——模型与动画

Zombie用作巡逻兵。

3D游戏(7)——模型与动画

当然,为给游戏增加一点趣味性,BlueIdol用作我们此次任务需要拯救的公主。手动狗头

动作的话,三者都是一样的,新建一个Animator Controllerperson

3D游戏(7)——模型与动画

设置好参数,状态,触发isWin的话就跳到win状态,触发isDead的话就跳到die状态,isRunning为true的话就转到状态run,否则保持在Initial State。

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画

四个状态的动作设置如上图所示,用的全都是素材包VoxelCharacters里面的动作。

毋庸置疑,事先需要把该添加的组件(动画,刚体,碰撞体),添加好。

3D游戏(7)——模型与动画

Zombie和BlueIdol的设置都是一样的,现在任务就是把前面下载到的玩家资源里面的模型更换掉。

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画

把原来的除了Camera Target以外的都可以删掉了,把Hero模型添加进来,同时为了方便,也可以把摄像机(素材包里面挂了脚本OrbitingCamera的摄像机,不是普通摄像机)拖进来。

这样一来直接运行的话肯定是bug一大堆,需要稍作修改。

3D游戏(7)——模型与动画

Camera Target的高度需要调调,

3D游戏(7)——模型与动画

脚本OrbitingCamera里面的Distance也需要调,

3D游戏(7)——模型与动画

脚本CharacterCapsule里面的胶囊碰撞体的半径和高度要调,这里因为在脚本里面有添加碰撞体,因而在Hero里面就不需要添加刚体和碰撞体组件。

3D游戏(7)——模型与动画

接下来就是修改脚本SimpleCharacterController里面的速度等参数,同时新增一个参数Hero。

3D游戏(7)——模型与动画

在运动的时候需要对hero的动画进行操作。同时将原来直接对于transform的操作全部改为hero.transform,比如输入方向键时对游戏物体方向的旋转,只需要Hero模型旋转即可。

3D游戏(7)——模型与动画

这么改改应该就可以了,不知道有没有遗漏的地方,具体详情可以观察我的具体代码,后续会给出仓库地址,这个素材包的脚本全都在Mente Bacata/Scivolo Character Controller文件夹里面,可以找找。

3D游戏(7)——模型与动画

这个动作有点憨憨

这么一来,可以说是场地,Player全都弄好了,接下来可以具体实现这个游戏了,可以在以往代码的基础上做修改,用的同样是MVC架构。

先从FirstController开始,

public interface IUserAction
{
    void Restart();
}

首先在这个游戏里面,IUserAction只需要一个Restart接口即可。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FirstController : MonoBehaviour, ISceneController, IUserAction {

	private bool isRuning;
	private int score;

	private CCActionManager actionManager;
	private GameEventManager eventManager = GameEventManager.GetInstance();
	private GameObject player;
	private GameObject BlueIdol;
    private List<GameObject> Zombies = new List<GameObject>();
	private int currentArea = 99;

	public GameObject plane;

	// the first scripts
	void Awake () {
		SSDirector director = SSDirector.getInstance ();
		director.setFPS (60);
		director.currentSceneController = this;
		director.currentSceneController.LoadResources ();
		this.gameObject.AddComponent<UserGUI>();
        this.gameObject.AddComponent<CCActionManager>();
		actionManager=this.gameObject.GetComponent<CCActionManager>();
		GameEventManager.onPlayerEnterArea+=OnPlayerEnterArea;
		GameEventManager.onZombieCollideWithPlayer+=OnZombieCollideWithPlayer;
		GameEventManager.onPlayerCollideWithBlueIdol+=OnPlayerCollideWithBlueIdol;
		score=0;
	}
	 
	// loading resources for the first scence
	public void LoadResources () {
		isRuning=true;
		// Map.LoadPlane();
		plane.SetActive(true);
		LoadZombies();
		LoadPlayer();
		LoadBlueIdol();
	}

	public bool GetIsRuning(){
		return isRuning;
	}

	public void JudgeCallback(bool isRuning,int score){
		this.score+=score;
        this.gameObject.GetComponent<UserGUI>().score=this.score;
        this.isRuning=isRuning;
    }

	public void Pause ()
	{
		throw new System.NotImplementedException ();
	}

	public void Resume ()
	{
		throw new System.NotImplementedException ();
	}

	private void OnPlayerEnterArea(int area){
		if(isRuning&&currentArea!=area){
			if(currentArea!=99){
				score++;
				this.gameObject.GetComponent<UserGUI>().score=this.score;
				Zombies[currentArea].GetComponent<Zombie>().isFollowing=false;
			}
			currentArea=area;
			Zombies[currentArea].GetComponent<Zombie>().isFollowing=true;
			actionManager.Trace(Zombies[currentArea],player);
		}
	}

	private void OnZombieCollideWithPlayer(){
		isRuning=false;
		player.transform.GetChild(1).gameObject.GetComponent<Animator>().SetTrigger("isDead");
		this.gameObject.GetComponent<UserGUI>().gameMessage="You Lose!!";
		player.tag="Finish";
		player.GetComponent<Rigidbody>().isKinematic=true;
		Zombies[currentArea].GetComponent<Zombie>().isFollowing=false;
		actionManager.Stop();
		for(int i=0;i<11;i++){
			Zombies[i].GetComponent<Animator>().SetTrigger("isWin");
		}
	}

	private void OnPlayerCollideWithBlueIdol(){
		isRuning=false;
		player.transform.GetChild(1).gameObject.GetComponent<Animator>().SetTrigger("isWin");
		BlueIdol.GetComponent<Animator>().SetTrigger("isWin");
		this.gameObject.GetComponent<UserGUI>().gameMessage="You Win!!";
		player.tag="Finish";
		player.GetComponent<Rigidbody>().isKinematic=true;
		Zombies[currentArea].GetComponent<Zombie>().isFollowing=false;
		actionManager.Stop();
		for(int i=0;i<11;i++){
			Zombies[i].GetComponent<Animator>().SetTrigger("isDead");
		}
	}

	private void LoadZombies(){
		GameObject ZombiePrefab=Resources.Load<GameObject>("Prefabs/Zombie");
		for(int i=0;i<11;i++){
			GameObject Zombie = Instantiate(ZombiePrefab);
			Zombie.AddComponent<Zombie>().area=i;
			Zombie.GetComponent<Rigidbody>().freezeRotation=true;
			Zombie.AddComponent<ZombieCollider>();
			Zombie.name="Zombie"+i;
			Zombie.GetComponent<Animator>().Play("Initial State");
			Zombie.GetComponent<Animator>().SetBool("isRunning",true);
			Zombie.transform.position=Map.center[i]+new Vector3(0,5,0);
			Zombies.Add(Zombie);
		}
	}

	private void LoadPlayer(){
		player=Instantiate(Resources.Load<GameObject>("Prefabs/Player"));
		player.GetComponent<Rigidbody>().freezeRotation=true;
		player.AddComponent<PlayerCollider>();
		player.transform.position=new Vector3(0,0,-380);
		player.tag="Player";
	}

	private void LoadBlueIdol(){
		BlueIdol=Instantiate(Resources.Load<GameObject>("Prefabs/BlueIdol"));
		BlueIdol.GetComponent<Rigidbody>().freezeRotation=true;
		BlueIdol.transform.position=new Vector3(0,0,140);
		BlueIdol.tag="Finish";
	}

	#region IUserAction implementation
	public void Restart ()
	{
		isRuning=true;
		this.gameObject.GetComponent<UserGUI>().gameMessage="";
		this.gameObject.GetComponent<UserGUI>().score=0;
		score=0;
		currentArea=99;
		player.transform.position=new Vector3(0,0,-380);
		player.GetComponent<Rigidbody>().isKinematic=false;
		player.transform.rotation=Quaternion.AngleAxis(0,Vector3.up);
		player.transform.GetChild(1).gameObject.GetComponent<Animator>().Play("Initial State");
		for(int i=0;i<11;i++)
		{
			Zombies[i].GetComponent<Animator>().Play("Initial State");
			Zombies[i].GetComponent<Animator>().SetBool("isRunning",true);
			Zombies[i].transform.position=Map.center[i]+new Vector3(0,5,0);
			Zombies[i].GetComponent<Zombie>().isFollowing=false;
			actionManager.GoAround(Zombies[i]);
		}
		player.tag="Player";
	}
	#endregion


	// Use this for initialization
	void Start () {
		//give advice first
	}
	
	// Update is called once per frame
	void Update () {
		if(isRuning){
			for(int i=0;i<11;i++){
				if(i!=currentArea){
					actionManager.GoAround(Zombies[i]);
				}
				else{
					Zombies[i].GetComponent<Zombie>().isFollowing=true;
					actionManager.Trace(Zombies[i],player);
				}
			}
		}
	}

}

Awake函数里面依旧是对各个变量的初始化,这一点毋庸置疑,同时因为我们使用订阅与发布模式,在GameEventManager里面我们会有玩家进入区域巡逻兵与玩家碰撞玩家与公主碰面等事件的处理,因而在FirstControllerAwake函数里面需要对这些事件进行赋值,以符合订阅与发布模式

LoadResources里包含有地形、巡逻兵、玩家、公主的加载,地形的话前面也有说到,做不成预制,无可奈何只能够通过SetActive这种蠢蠢的方式。玩家、公主的加载只是普通的游戏对象预制实例化过程,并作相应的初始化操作。而巡逻兵的加载,虽说是需要用工厂模式,但是在这里,考虑到只是在最初的时候产生出来就完事了,后续都不会再有添加/删除操作了,因而偷了一下懒,只是把预制加载出一个游戏对象,再把这个游戏对象实例化11个出来就完事了。

接下来就是玩家进入区域巡逻兵与玩家碰撞玩家与公主碰面三种事件的处理函数,OnPlayerEnterArea表明玩家进入了一个新的区域,这个区域内的巡逻兵就会来追你,旧的那个区域的巡逻兵也就被甩开了,不再追逐,分数+1;OnZombieCollideWithPlayer的话,玩家就被追上了,游戏就结束了,输了;OnPlayerCollideWithBlueIdol表明玩家已经“救出”公主了,游戏也就结束了,巡逻兵不再追逐,玩家胜利。

Restart函数则是对游戏参数恢复初始状态,巡逻兵、玩家等位置也恢复回初始状态。

Update函数主要是针对巡逻兵而言,当前处在追逐状态的巡逻兵依旧在追逐,否则继续巡逻。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameEventManager
{
    private static GameEventManager _instance;

    public delegate void OnPlayerEnterArea(int area);
    public static event OnPlayerEnterArea onPlayerEnterArea;

    public delegate void OnZombieCollideWithPlayer();
    public static event OnZombieCollideWithPlayer onZombieCollideWithPlayer;

    public delegate void OnPlayerCollideWithBlueIdol();
    public static event OnPlayerCollideWithBlueIdol onPlayerCollideWithBlueIdol;

    private GameEventManager()
    {

    }

    public static GameEventManager GetInstance()
    {
        if (_instance == null) {
			_instance = new GameEventManager();
		}
		return _instance;
    }

    public void PlayerEnterArea(int area)
    {
        onPlayerEnterArea?.Invoke(area);
    }

    public void ZombieCollideWithPlayer()
    {
        onZombieCollideWithPlayer?.Invoke();
    }

    public void PlayerCollideWithBlueIdol()
    {
        onPlayerCollideWithBlueIdol?.Invoke();
    }
}

GameEventManager就是玩家进入区域巡逻兵与玩家碰撞玩家与公主碰面三种事件的处理,根据FirstController发不出来的内容来处理。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Map : Object
{
    private static GameObject planePrefab=Resources.Load<GameObject>("Prefabs/Plane");
    public static Vector3[] center=new Vector3[]{new Vector3(-55,0,-300),new Vector3(0,0,-300),new Vector3(55,0,-300),new Vector3(-55,0,-140),new Vector3(0,0,-115),new Vector3(55,0,-90),new Vector3(-30,0,-17),new Vector3(30,0,-17),new Vector3(0,0,50),new Vector3(55,0,120),new Vector3(-55,0,120)};

    public static void LoadPlane(){
        GameObject map=Instantiate(planePrefab);
    }
}

前面巡逻兵加载、Restart等函数里面都涉及到有位置属性,这些都保存在Map里面,如果地形需要从预制加载的话,也是在这里实现。

接下来就到细致一点的了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Zombie : MonoBehaviour
{
    public int area;
    public bool isFollowing=false;

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Zombie里面存的是当前巡逻兵处在的巡逻区域、是否处在追逐状态。就好比前一个打飞碟作业的UFOData的作用。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AreaCollider : MonoBehaviour
{

    public int area;

    public void OnTriggerEnter(Collider collider){
        if(collider.gameObject.tag=="Player"){
            GameEventManager.GetInstance().PlayerEnterArea(area);
        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

AreaCollider要挂载到地图的11个区域里面,当这个区域里面检测到有碰撞体Player进来的时候就调用GameEventManager里面的PlayerEnterArea函数。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ZombieCollider : MonoBehaviour
{

    public void OnCollisionEnter(Collision collision){
        if(collision.gameObject.tag=="Player"){
            GameEventManager.GetInstance().ZombieCollideWithPlayer();
        }
        else{

        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

ZombieCollider则是检测跟Player的碰撞,然后调用ZombieCollideWithPlayer函数。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerCollider : MonoBehaviour
{

    public void OnCollisionEnter(Collision collision){
        if(collision.gameObject.tag=="Finish"){
            GameEventManager.GetInstance().PlayerCollideWithBlueIdol();
        }
        else{

        }
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

PlayerCollider也是类似的操作。

如此一来,剩下的就只有动作的实现了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCActionManager : SSActionManager, ISSActionCallback
{

    private int currentArea=-1;
    Dictionary<int,CCMoveToAction> moveToActions=new Dictionary<int,CCMoveToAction>();

    protected new void Start()
    {
        
    }

    public void Trace(GameObject Zombie,GameObject player){
        var area=Zombie.GetComponent<Zombie>().area;
        if(area==currentArea){
            return;
        }
        currentArea=area;
        if(moveToActions.ContainsKey(area)){
            moveToActions[area].destroy=true;
        }
        CCTraceAction action=CCTraceAction.GetSSAction(player,25);
        this.RunAction(Zombie,action,this);
    }

    public void GoAround(GameObject Zombie){
        var area=Zombie.GetComponent<Zombie>().area;
        if(moveToActions.ContainsKey(area)){
            return;
        }
        var target=GetGoAroundTarget(Zombie);
        CCMoveToAction action=CCMoveToAction.GetSSAction(target,12,area);
        moveToActions.Add(area,action);
        this.RunAction(Zombie,action,this);
    }

    public void Stop(){
        foreach(var x in moveToActions.Values){
            x.destroy=true;
        }
        moveToActions.Clear();
        currentArea=-1;
    }

    private Vector3 GetGoAroundTarget(GameObject Zombie){
        Vector3 pos=Zombie.transform.position;
        var area=Zombie.GetComponent<Zombie>().area;
        float x_down=Map.center[area].x-27.5f;
        float x_up=Map.center[area].x+27.5f;
        float z_down=Map.center[area].z-40;
        float z_up=Map.center[area].z+40;
        var move=new Vector3(Random.Range(-15,15),0,Random.Range(-15,15));
        var next=pos+move;
        int tryCount=0;
        while(!(next.x>x_down&&next.x<x_up&&next.z>z_down&&next.z<z_up)||next==pos){
            move=new Vector3(Random.Range(-15,15),0,Random.Range(-15,15));
            next=pos+move;
            if((++tryCount)>100){
                next=Map.center[area];
                break;
            }
        }
        return next;
    }

    public void SSActionEvent(SSAction source,
        SSActionEventType events=SSActionEventType.Competed,
        int intParam=0,
        string strParam=null,
        Object objectParam=null){
            var area=source.gameObject.GetComponent<Zombie>().area;
            if(moveToActions.ContainsKey(area)){
                moveToActions.Remove(area);
            }
        }
}

在先前的CCActionManager上做修改。同时需要用一个Dictionary来记录正在执行过程中的动作,因为好比巡逻兵的巡逻,它需要到达一个目标点之后才转向另一个目标点前进。回调函数就是将那个记录从Dictionary清除。

巡逻的目标点则是通过函数GetGoAroundTarget来完成,通过一种随机取点的方式。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCMoveToAction : SSAction
{
    public Vector3 target;
    public float speed;
    public int area;

    public static CCMoveToAction GetSSAction(Vector3 target, float speed, int area){
        CCMoveToAction action=ScriptableObject.CreateInstance<CCMoveToAction>();
        action.target=target;
        action.speed=speed;
        action.area=area;
        return action;
    }

    // Start is called before the first frame update
    public override void Start()
    {
        if(target-gameObject.transform.position!=Vector3.zero){
            Quaternion rotation=Quaternion.LookRotation(target-gameObject.transform.position,Vector3.up);
            gameObject.transform.rotation=rotation;
        }
    }

    // Update is called once per frame
    public override void Update()
    {
        if((gameObject.transform.position-target).magnitude<0.1f){
            destroy=true;
            callback.SSActionEvent(this);
        }
        else{
            gameObject.transform.position=Vector3.MoveTowards(gameObject.transform.position,target,speed*Time.deltaTime);
        }
    }
}


CCMoveToAction实现的是巡逻兵每次巡逻的走动。跟先前牧师与魔鬼的实现有些许类似,向一个固定的目的地移动,但是需要涉及方向的转动。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCTraceAction : SSAction
{

    public GameObject target;
    public float speed;

    public static CCTraceAction GetSSAction(GameObject target,float speed){
        CCTraceAction action = CreateInstance<CCTraceAction>();
        action.target = target;
        action.speed = speed;
        return action;
    }

    // Start is called before the first frame update
    public override void Start()
    {
        
    }

    // Update is called once per frame
    public override void Update()
    {
        gameObject.transform.position=Vector3.MoveTowards(gameObject.transform.position,target.transform.position,speed*Time.deltaTime);
        if(gameObject.GetComponent<Zombie>().isFollowing==false||(gameObject.transform.position-target.transform.position).sqrMagnitude<0.1f){
            destroy=true;
            callback.SSActionEvent(this);
        }
        else{
            Quaternion rotation=Quaternion.LookRotation(target.transform.position-gameObject.transform.position,Vector3.up);
            gameObject.transform.rotation=rotation;
        }
    }
}

CCTraceAction则是要朝向Player移动。

最后,UserGUI直接套用先前的差不多了,游戏可以快乐玩耍了。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UserGUI : MonoBehaviour {

	private IUserAction action;
	public string gameMessage;
	public int score;

	void Start () {
		action = SSDirector.getInstance ().currentSceneController as IUserAction;
		score=0;
	}

	void OnGUI() {  
		float width = Screen.width / 6;  
		float height = Screen.height / 12;

		//action.Check();
		GUIStyle style = new GUIStyle();
        style.normal.textColor = Color.red;
        style.fontSize = 30;
		GUI.Label(new Rect(320,100,50,200),gameMessage,style);
		GUI.Label(new Rect(width*2, 0, width, height),"Score: ",style);
		GUI.Label(new Rect(width*3, 0, width, height),score.ToString(),style);

		if (GUI.Button(new Rect(0, 0, width, height), "Restart")) {  
			action.Restart();  
		} 

		string paused_title = SSDirector.getInstance ().Paused ? "Resume" : "Pause!"; 
		if (GUI.Button(new Rect(width, 0, width, height), paused_title)) { 
			SSDirector.getInstance ().Paused = !SSDirector.getInstance ().Paused;
		} 
	}


}

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画

3D游戏(7)——模型与动画

我的完整代码

相关标签: 3d游戏