unity3d的第一人称射箭小游戏

一、游戏玩法与介绍

第一人称射箭小游戏是基于unity3d编写的小游戏。玩家视角为一个弓弩,通过鼠标操控弓弩的视角方向,同时通过键盘的上下左右键或者“wasd”键来控制弓弩的移动方向。地图中有一个初始位置和两个靶场,玩家一开始降临在初始位置,按下1键进入静态靶场,里面有两种靶子,一大一小,共8个。按下2键进入动态靶场,里面同样有两种靶子,一种运动速度较快、变化多,一种运动速度较慢,且为直线运动。

游戏需求:

  • 地形:使用地形组件,上面有山、路、草、树
  • 天空盒:使用天空盒,天空可随特定按键切换天空盒
  •  固定靶:使用静态物体,有一个以上固定的靶标;(注:射中后状态不会变化)
  •  运动靶:使用动画运动,有一个以上运动靶标,运动轨迹,速度使用动画控制;(注:射中后有效果或自然落下)
  •  射击位:地图上标记了两个射击区域,仅在射击区域可以拉弓射击;
  • 摄像机:有一个瞄准镜,使得游戏更加易于操控;
  • 声音:使用声音组件,播放背景音 与 箭射出的声效;
  • 运动与物理与动画
  • 游走:使用第一人称组件,玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
  • 射击效果:使用 物理引擎 或 动画 或 粒子,运动靶被射中后产生适当效果。
  • 碰撞与计分:使用 计分类 管理规则,在射击位射中靶标得相应分数,规则自定;
  • 驽弓动画:使用 动画机 与 动画融合, 实现十字驽蓄力半拉弓,然后 hold,择机 shoot

计分规则如下(从左到右编号):

固定靶:

1234
大(近)5432
小(远)10111213

运动靶:

慢速靶15
快速靶20

二、开发游戏的准备

2.1 UML图

2.2 各个类接口的作用

首先,游戏由导演、场记、运动管理师、演员构成。在第一人称射箭游戏设计中,场记引入了记分员(ScoreRecorder)、箭管理员(ArrowController)。箭管理员管理箭的发放与回收,采用对象池的方式管理这些箭;记分员按箭的数据计分,记分员拥有计分规则。

同时,这里引入了一个对象池ArrowFactory,它是一个单实例的类,运用场景单实例的方法在ArrowController中创建。在对象池中,我设计了方法get_arrow()来产生飞碟,还有方法recycle_arrow(arrow)来回收箭,这样可以避免不断创造与销毁预制对象arrow时产生的内存空间损耗。

下面是对象池技术(ArrowFactory)的代码:

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

public class ArrowFactory : MonoBehaviour
{
    public ArrowController arrow_ctrl;
    public GameObject arrow;
    // Start is called before the first frame update
    void Start()
    {
        arrow_ctrl = (ArrowController)SSDirector.getInstance().currentSceneController;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    public void recycle_arrow(GameObject arrow)//回收
    {
        arrow.SetActive(false);
        DestroyImmediate(arrow);
    }

    public GameObject get_arrow()//获取
    {
        arrow = GameObject.Instantiate(Resources.Load("Prefabs/Arrow", typeof(GameObject))) as GameObject;//得到弓上箭的位置
        Transform bow_mid = arrow_ctrl.bow.transform.GetChild(4);
        arrow.transform.position = bow_mid.transform.position;//将箭放到弓的中间
        arrow.transform.rotation = arrow_ctrl.bow.transform.rotation;//将箭与弓平行
        arrow.transform.parent = arrow_ctrl.bow.transform;//跟随弓运动
        arrow.gameObject.SetActive(false);
        return arrow;
    }
}

2.3 游戏的对象

2.3.1 地形

地形采用了unity3d官方给出的Terrain对象,按照提示信息我们可以设置地图的大小以及其他各种属性,同时还能给地图上色、种花草树木,造山造盆地等等操作。其中,纹理采用的是fantasy sky free资源的草地与黄土,以及自带的TerrainRock,这个是用来给地图上色的,搭配起来可以有一种山村小路的感觉。而白色的TerrainRock则用来标记射箭点位。

花草采用了fantasy sky free中的Brush_Flower。

树木采用了Proxy Games资源包中的一些树、岩石、灌木以及蘑菇,让地图看起来更加美观。为了让视角移动的时候不能碰到树和岩石,我在他们的预制件里面增加了一个胶囊碰撞器和盒碰撞器。同时,在弓弩、摄像机以及存放弓弩的立方体对象中都要加上盒碰撞器,这样才能检测碰撞,不让视角撞树。同时可以实现爬山。

造山的时候,可以选择Raise or Lower Terrain 组件,就可以渲染出山或者盆地了。

最后,整个地形如下

2.3.2 天空盒

第一人称射箭小游戏实现了按键换景,即不同靶场的天空盒不一样。

天空盒的材料选的是Fantasy Sky Free,添加天空盒的方法可以参考我前几篇博客,这里不再赘述。

2.3.3 场景声音

本游戏的背景音乐采用了汪苏泷的我想念。首先,将MP3文件放入Assets文件中,然后在胶囊体对象中增加一个组件Audio Source,将音乐放在音乐片段AudioClip上即可。

2.3.4 弓弩

制造一个弓弩很困难,但我们可以利用现有的免费资源包RyuGiKen,这是一个经典的弓弩与弓箭的资源包Classical CrossBow。将这个资源包下载并导入进来之后,由于整局游戏的弓弩只有一个,因此我们可以直接将其放置在场景中。首先创建一个胶囊立方体对象,接着将RyuGiKen文件下的整个Crossbow文件拖入这一对象即可。绑定之后,我们还需要将摄像机也放到这一对象之下,这样我们就可以实现第一人称持弓。

同时,弓弩射箭动作还需要一个动画实现,以下便是弓弩的动画,实现了拉弓(pull),保持蓄力(hold),射箭(shoot)三个动作。

2.3.5 箭

就像前文所说,为了节省资源,我们需要用对象池的技术去创建和销毁箭,因此,我们需要将箭放置在预制文件中。同时,因为中靶是通过碰撞来检测的,因此需要给箭添加一个盒碰撞器

2.3.6 固定靶

按1键进入固定靶区域。固定靶有大一点的big_target以及小一点的target,对应的分数不同,预制如下:

2.3.7 运动靶

按2键进入运动靶区域。运动靶有快一点的target_quick和慢一点的target_move,对应分数不同。

运动靶需要用到动画技术,通过录制其运动轨迹来实现动画效果。首先,选择一个运动靶预制件,将其放入场景中,接着,点击Window下面的Animation,就可以出现录制的窗口了。这里你可以随意增加关键帧,让靶子自由地运动。由于是定点射击,因此我只让靶子的位置发生改变,但通过Animation组件还可以让其进行类似旋转之类的动作。

运动靶的预制如下:

2.4 文件组成

assets文件下有这些,其中Fantasy Sky Free、Proxy Games和RyuGiKen均为游戏要使用到的资源包,Animations是动作管理器,music存放了要用到的音乐,Resources中存放了材料和预制件,Scripts为脚本文件。

Animations中存放了弓弩动画,以及两种运动靶的动画

music存放了场景的音乐(汪苏泷的我想念)以及拉弓射箭的声音

Resources中存放了弓和各种靶子的预制件,还有场景可能要用到的材料,包括天空盒材料以及颜色材料。

Scripts文件下存放了脚本文件。本次编程仍然按照MVC模式以及动作分离规则进行代码编写。

Actions目录下包含了控制动作的接口与控制器的脚本;

Controllers目录下包含了各个控制场景的接口与控制器脚本;

Views目录下包括了与用户交互相关的接口与控制器脚本。

三、代码部分

3.1 动作(Actions)

3.1.1 ISSActionCallback

ISSActionCallback为一个回调函数的接口,用来发送各种消息。

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

public enum SSActionEventType:int { Started, Competeted }

public interface ISSActionCallback
{
	void SSActionEvent(SSAction source, 
		SSActionEventType events = SSActionEventType.Competeted,
		int intParam = 0 , 
		string strParam = null, 
		Object objectParam = null);
}


3.1.2 SSAction

SSAction是动作基类,继承了ScriptableObject类,定义了enable与destroy两个布尔变量,用来判断动作的执行与销毁。由于他是基类,因此后续所有的动作都继承于此。与之前不同的是,这次多定义了一个更适用于物理引擎更新的函数FixedUpdate()。

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

public class SSAction : ScriptableObject {

	public bool enable = true;//是否正在被进行
	public bool destroy = false;//是否销毁

	public GameObject gameobject { get; set; }//对象
	public Transform transform { get; set; }//配置信息
	public ISSActionCallback callback { get; set; }//回调函数

	protected SSAction () {}

	// Use this for initialization
	public virtual void Start () {
		throw new System.NotImplementedException ();
	}
	
	// Update is called once per frame
	public virtual void Update () {
		throw new System.NotImplementedException ();
	}
	public virtual void FixedUpdate()
    {
        throw new System.NotImplementedException();
    }	
}

3.1.3 SSActionManager


SSActionManager是动作管理器的基类,管理所有动作。有一个动作字典actions用来添加所有需要执行的动作,waitingAdd列表用来存储等待加入到字典的动作,waitingDelete列表用来存储等待摧毁的动作对象。在update函数中,先是遍历waiting列表,将需要执行的动作加入到字典中;然后遍历字典,判断动作是否可以执行还是执行完成需要摧毁;最后遍历waitingDelete列表中的每个动作并将它们摧毁。同时,还定义了RunAction函数来创建动作、绑定游戏对象和动作以及该动作实现的消息接收者,添加到列表中并执行。最后,需要注意的是,这里实现了函数FixedUpdate()

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

public class SSActionManager : MonoBehaviour {

	private Dictionary <int, SSAction> actions = new Dictionary <int, SSAction> ();//动作的字典集
	private List <SSAction> waitingAdd = new List<SSAction> (); //等待添加的动作
	private List<int> waitingDelete = new List<int> ();//等待销毁的动作

	// Update is called once per frame
	protected void Update () {
        for (int i = 0; i < waitingAdd.Count; ++i)
        {
            actions[waitingAdd[i].GetInstanceID()] = waitingAdd[i];
        }
        waitingAdd.Clear ();

		foreach (KeyValuePair <int, SSAction> kv in actions) {
			SSAction ac = kv.Value;
			if (ac.destroy) { 
				waitingDelete.Add(ac.GetInstanceID()); // release action
			} else if (ac.enable) { 
				ac.Update (); // update action
			}
		}

		foreach (int key in waitingDelete) {
			SSAction ac = actions[key]; 
			actions.Remove(key); 
			Object.Destroy(ac);
		}
		waitingDelete.Clear ();
	}

	public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
		action.gameobject = gameobject;
		action.transform = gameobject.transform;
		action.callback = manager;
		waitingAdd.Add (action);
		action.Start ();
	}
    //物理引擎
    protected void FixedUpdate()
    {
        for (int i = 0; i < waitingAdd.Count; ++i)
        {
            actions[waitingAdd[i].GetInstanceID()] = waitingAdd[i];
        }
        waitingAdd.Clear();
        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            if (ac.destroy)
            {
                waitingDelete.Add(ac.GetInstanceID());
            }
            else if (ac.enable)
            {
                ac.FixedUpdate();
            }
        }

        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            Object.Destroy(ac);
        }
        waitingDelete.Clear();
    }

    // Use this for initialization
    protected void Start () {
	}

    public void SSActionEvent(SSAction source, int param = 0, GameObject arrow = null)
    {
        //回调新动作类
    }
}

3.1.4 CCShootAction

CCShootAction是SSAction的具体实现。首先,定义了一个变量pulse_force,为射箭的冲量,即为方向*力度。start()函数用于初始化,先让箭不跟随弓移动,设置初速度为0,开启重力势能,随后给箭一个冲量,最后还需要关闭运动学控制。FixdUpdate()函数用来断判断箭是否飞出场景、落在地上或者中靶。如果满足上述的任何一种,则说明该动作完成了,销毁动作。

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

public class CCShootAction : SSAction
{
	public Vector3 pulse_force;
	private CCShootAction() { }

	public static CCShootAction GetSSAction(Vector3 impulse_direction, float power){
        CCShootAction action = CreateInstance<CCShootAction> ();
		action.pulse_force = impulse_direction.normalized * power;//冲量=方向*力度
		return action;
	}

	public override void Update ()
	{
        
    }

	public override void Start ()
    {
        gameobject.transform.parent = null;//不再跟着弓弩移动
        gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero;//初速度为0
        gameobject.GetComponent<Rigidbody>().useGravity = true;//开启重力势能
        gameobject.GetComponent<Rigidbody>().AddForce(pulse_force, ForceMode.Impulse);//给发射的箭添加一个冲量
        gameobject.GetComponent<Rigidbody>().isKinematic = false;//关闭运动学控制
	}

    public override void FixedUpdate()// 判断箭的位置,在地上还是在靶子上,还是飞出去了
    {   
        if (!gameobject || this.gameobject.tag == "ground" || this.gameobject.tag == "onTarget")
        {
            this.destroy = true;// 摧毁动作
            this.callback.SSActionEvent(this);
        }
    }
}

3.1.5 CCActionManager

CCActionManager是SSActionManager的具体实现,继承了SSActionManager与ISSActionCallback。这里将射箭的动作实例化,调用SSActionManager的函数RunAction()来运行射箭动作。

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

public class CCShootManager : SSActionManager, ISSActionCallback {
	
	private CCShootAction shoot;

	protected new void Start() {
	}

	// Update is called once per frame
	//protected new void Update ()
	//{
		//base.Update ();
	//}
		
	public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
	{
		
	}

    public void ArrowShoot(GameObject arrow, Vector3 impulse_direction, float power)// 游戏对象、力的方向、力的大小(控制箭的飞行)
    {
        shoot = CCShootAction.GetSSAction(impulse_direction, power);//射箭动作实例化。
        RunAction(arrow, shoot, this); //运行动作。
    }

}

3.2 控制器(Controllers)

3.2.1 SSDirector

SSDirector类继承自System.Object,是整个游戏的导演,控制场景与全局。

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

// 导演类,控制场景切换
public class SSDirector : System.Object
{
    private static SSDirector _instance;
    public ISceneController currentSceneController { get; set; }

    public static SSDirector getInstance()
    {          // 获取导演实例
        if (_instance == null)
        {
            _instance = new SSDirector();
        }
        return _instance;
    }
}

3.2.2 ISceneController

ISceneController作为场景的接口,提供了加载资源函数LoadResources()

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

// 场景控制器接口,与场景控制有关的函数都在这里
public interface ISceneController
{
    void LoadResources();    // 加载资源
}

3.2.3 Singleton

Singleton是一个单实例的模板,其定义为:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。定义好了这一个模板之后,你仅需要将 MonoBehaviour 子类对象挂载任何一个游戏对象上即可。然后在任意位置上使用代码

Singleton<YourMonoType>.Instance 即可获得该对象获得该对象。

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

//场景单实例模板
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{

	protected static T instance;

	public static T Instance
	{
		get
		{
			if (instance == null)
			{
				instance = (T)FindObjectOfType(typeof(T));
				if (instance == null)
				{
					Debug.LogError("An instance of " + typeof(T) +
					" is needed in the scene, but there is none.");
				}
			}
			return instance;
		}
	}
}

比如,在场景主控制器中,以下两行代码则用单实例获得了箭工厂以及得分记录。

3.2.4 ArrowFactory

ArrowFactory为一个箭的对象池,对象池技术前文已有提及,这里不再赘述。也可以看我的上一篇博客。链接如下:

unity3d 的鼠标打飞碟小游戏(Hit UFO)-CSDN博客

3.2.5 TargetController

TargetController为靶子控制器,继承自MonoBehaviour。首先定义了靶子的分值Ring_Score,初始化为0,以及一个得分记录器sc_recorder。场景开始时,用单实例创建一个得分纪录器。

靶子主要的功能就是碰撞检测,这里用到了碰撞检测函数OnCollisionEnter(),如果有箭射中靶子,则同时进行以下动作:设置箭的速度为0;设置箭的旋转角度为0;将箭和靶子绑定;统计分数;设置为标签为中靶

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

public class TargetController : MonoBehaviour
{
    public int Ring_Score = 0;//环的分值,初始化0
    public ScoreRecorder sc_recorder;
    // Start is called before the first frame update
    void Start()
    {
        sc_recorder = Singleton<ScoreRecorder>.Instance;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    void OnCollisionEnter(Collision collision)
    {
        Transform arrow = collision.gameObject.transform;
        if (arrow == null)
        {
            return;
        }
        if (arrow.tag == "arrow")
        {
            arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);//设置速度为0
            arrow.transform.rotation = Quaternion.Euler(0, 0, 0);//设置旋转角度为0
            arrow.GetComponent<Rigidbody>().isKinematic = true;//启动运动学
            arrow.transform.parent = this.transform;// 将箭和靶子绑定
            sc_recorder.Record_Score(Ring_Score);//计分
            arrow.tag = "onTarget";//设置为中靶
        }
    }
}

3.2.6 BowController

BowController为弓控制器。这里设置了弓的角速度、移动速度、水平灵敏度、垂直灵敏度、x方向旋转角和一个布尔值判断是否可以射箭。同时引入了一个刚体rb。

Move()函数:读取方向键的输入并以此移动游戏对象。移速我只设置了匀速直线运动,即距离=速度*时间。

SetCursorToCentre()函数:使鼠标锁定在屏幕中心

View()函数:控制视角,分水平转动和垂直转动,这个视角与鼠标的位置、角速度和灵敏度有关。由于本人晕3d,因此灵敏度设置的较低,而且垂直方向的旋转角限制在(-45°,45°)之间,可以适当调整。

transport()函数:将玩家传送到指定的射击位置,每个位置都有不同的天空盒。按1传送到固定靶靶场;按2传送到动态靶靶场;按3传送回初始位置;按5可以去看风景的位置。

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

public class BowController : MonoBehaviour
{
    public float angular_speed = 80;//角速度
    public float move_speed = 2;//移动速度
    public float jump_force = 200f;//跳跃
    public float horizontal_rotate_sensitivity = 6;//水平灵敏度
    public float vertical_rotate_sensitivity = 6;//垂直灵敏度
    public bool can_shoot = false;//是否可以射箭
    public float xRotation = 0f;//x方向旋转角
    private Rigidbody rb;
    // Start is called before the first frame update
    void Start()
    {
        rb = transform.parent.GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void FixedUpdate()
    {
        Move();
        View();
        transport();
    }
    public float jump_height = 10.0f;
    void Move()//移动
    {
        float v = Input.GetAxis("Vertical");
        float h = Input.GetAxis("Horizontal");
        transform.parent.Translate(Vector3.forward * v * Time.deltaTime * move_speed);
        transform.parent.Translate(Vector3.right * h * Time.deltaTime * move_speed);
        if (Input.GetKeyDown(KeyCode.Space))
        {
            rb.AddForce(Vector3.up * Mathf.Sqrt(jump_height * -2.0f * Physics.gravity.y), ForceMode.VelocityChange);
        }
    }

    void SetCursorToCentre()// 让鼠标锁定在屏幕中心
    {
        //锁定鼠标后再解锁鼠标,鼠标将自动回到屏幕中心
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.lockState = CursorLockMode.None;
        Cursor.visible = false;//隐藏鼠标
    }

    void View()//视角
    {
        SetCursorToCentre();
        float mouseX = Input.GetAxis("Mouse X") * Time.deltaTime * angular_speed * horizontal_rotate_sensitivity;
        transform.parent.Rotate(Vector3.up * mouseX);//水平旋转
        float mouseY = Input.GetAxis("Mouse Y") * Time.deltaTime * angular_speed * vertical_rotate_sensitivity;
        xRotation = xRotation - mouseY;
        xRotation = Mathf.Clamp(xRotation, -45f, 45f);//限制旋转的上下视角
        transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);//人保持不动
    }

    void transport()//传送到指定的位置
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))// 静态靶
        {
            can_shoot = true;
            transform.parent.rotation = Quaternion.Euler(0, 0, 0);
            transform.parent.position = new Vector3(335, 3.6f, 290);
            RenderSettings.skybox = Resources.Load<Material>("Materials/SkyboxMaterial1");
        }
        if (Input.GetKeyDown(KeyCode.Alpha2))// 动态靶
        {
            can_shoot = true;
            transform.parent.rotation = Quaternion.Euler(0, 180, 0);
            transform.parent.position = new Vector3(290, 3.6f, 175);
            RenderSettings.skybox = Resources.Load<Material>("Materials/SkyboxMaterial2");
        }
        if (Input.GetKeyDown(KeyCode.Alpha3))//回到初始位置
        {
            can_shoot = false;
            transform.parent.position = new Vector3(195, 3.6f, 320);
            RenderSettings.skybox = Resources.Load<Material>("Materials/SkyboxMaterial3");
        }
        if (Input.GetKeyDown(KeyCode.Alpha5))//看风景
        {
            can_shoot = false;
            transform.parent.position = new Vector3(205, 10.5f, 410);
        }
    }
}

3.2.7 ScoreRecorder

ScoreRecorder为计分控制器,用来记录得分,初始化得分为0。这里只有总分自增的逻辑。

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

public class ScoreRecorder : MonoBehaviour
{
    // Start is called before the first frame update
    public int score;
    void Start()
    {
        score = 0;
    }
    
    public void Record_Score(int ring_score)
    {
        score = score + ring_score;
    }
}

3.2.8 ArrowController

ArrowController为主场景控制器,也可以成为射箭控制器,因为他管理了箭的创建与发射事件。不仅继承于MonoBehaviour,还与场景和用户交互,继承于ISceneController于IUserAction。定义的变量如下方代码中注释所示。要注意的是,由于游戏要求有半拉弓的状态,因此需要一个变量state来区分此时弓弩的状态。0为普通状态,1为蓄力状态,2为蓄力状态。同时,因为拉弓射箭需要播放对应音效,因此还需要一个音源变量和两个媒体片段变量。

LoadTarget()函数:加载靶子的函数,这里需要给靶子增加一个靶子控制器的组件,用来判断是否中靶以及计分。

LoadArrow()函数:加载箭的函数,从箭对象池中循环得到箭,每次得到10支箭。

LoadResources()函数:加载场景。将靶子加载到不同的地方,同时给靶子赋分,具体分值前文有提及。

Start()函数:加载资源,为场景添加各种组件。注意这里有一个动画组件,必须加上运动靶才能生效。

Shoot()函数:管理射箭状态。当背包中有箭时,如果没有按1、2,则不能射箭。

Hit_Ground()函数:检查箭是否射中地面。如果与地面相交,则将其速度设为0,同时开启运动学控制。

Update()函数:用来循环检查箭的状态,先检查射箭状态(是否可以射箭),再检查箭是否在地面上。接着,回收一些地上的箭。同时,设置了按“L”键可以补充背包的箭数。

aniShoot()函数:根据玩家不同的鼠标点击动作,执行拉弓、等待、射箭的动画效果。状态变量state确保l动作是按照拉弓、等待、射箭的顺序来完成的,只有在正确状态下按下鼠标,才会执行相应的动画效果。对于“拉弓”,根据玩家按下鼠标左键的时间长短,弓箭的力也会改变,如果拉弓时间超过两秒,则按两秒的力来算;不同大小的力量,对应的动画的效果和射箭的力度也会不同。当处于等待状态并按下鼠标右键时,执行“射箭”,在播放动画的同时,从箭矢队列中取出一支箭,将其加入已发射flyed队列中,并通过射击动作管理器为这支箭执行射击动作。因此,操作方面,长按左键来拉弓,放开左键,点击右键即可发射。

play_sound_pull()函数:拉弓音效。

play_sound_shoot()函数:射箭音效。

这里需要在编辑器中将对应的音乐放入脚本中。比如,我将脚本放在了胶囊体对象中,在Pull_sound和Shoot_sound的最右边选择加入对应音乐即可。

get_arrow_num()函数:返回剩余的箭数量。

get_score()函数:返回当前分数。

get_message()函数:返回提示信息。

这三个函数用于用户交互控制器中,显示给玩家看。

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

public class ArrowController : MonoBehaviour, ISceneController, IUserAction
{
    public CCShootManager arrow_manager;//箭的动作管理者
    public ArrowFactory factory;//箭工厂
    public GameObject main_camera;//主相机
    
    public GameObject bow;//弓
    public GameObject arrow;//箭
    public GameObject target_a, target_b, target_c, target_d;//靶子
    public ScoreRecorder recorder;//计分器
    private int arrow_num = 0;//身上箭的数量
    private List<GameObject> flyed = new List<GameObject>();//飞出去的箭队列
    private List<GameObject> arrows = new List<GameObject>();//箭队列
    public Animator ani;//动画控制器
    public string mes = "";//显示的信息
    //拉弓控制数值
    public float longpress_duration = 2.0f;//设置长按持续时间
    public int state = 0;//普通:0,拉弓:1,蓄力:2
    public float power = 0f;//力度
    private bool is_longpressing = false;//判断是否在长按中
    private float press_time = 0f;//长按的时间
    //音效
    public AudioSource audio_source;//音源
    public AudioClip pull_sound;//拉弓音频
    public AudioClip shoot_sound;//射箭音频
    public void LoadResources()
    {
        main_camera = transform.GetChild(5).gameObject;
        bow = this.gameObject;
        for(int i = 0; i < 4; i++)
        {
            target_a = LoadTarget("target", 10 + i, new Vector3(300 + 20 * i, 20, 320 + 5 * i));//静态小靶子
            target_b = LoadTarget("big_target", 5 - i, new Vector3(300 + 20 * i, 10, 320 - 5 * i));//静态大靶子
        }
        target_c = LoadTarget("target_move", 15, new Vector3(0, 0, 0));//慢速移动靶
        target_d = LoadTarget("target_quick", 20, new Vector3(0, 0, 0));//快速移动靶
    }

    GameObject LoadTarget(string name, int score, Vector3 pos)
    {
        GameObject target = GameObject.Instantiate(Resources.Load("Prefabs/" + name, typeof(GameObject))) as GameObject;//预制
        target.transform.position = pos;
        target.AddComponent<TargetController>();// 给靶子添加中靶组件
        target.GetComponent<TargetController>().Ring_Score = score;
        return target;
    }

    // Start is called before the first frame update
    void Start()//加载资源
    {
        arrow = null;
        SSDirector director = SSDirector.getInstance();
        director.currentSceneController = this;
        director.currentSceneController.LoadResources();

        gameObject.AddComponent<ArrowFactory>();
        gameObject.AddComponent<BowController>();
        gameObject.AddComponent<ScoreRecorder>();
        gameObject.AddComponent<UserGUI>();
        
        factory = Singleton<ArrowFactory>.Instance;
        recorder = Singleton<ScoreRecorder>.Instance;
        arrow_manager = this.gameObject.AddComponent<CCShootManager>() as CCShootManager;
        ani = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        Shoot();//检查射箭
        Hit_Ground();//检查是否射中地面
        for(int i = 0; i < flyed.Count; i++)
        {
            GameObject tt_arrow = flyed[i];
            if (flyed.Count > 10 || tt_arrow.transform.position.y < -10)
            {
                flyed.RemoveAt(i);
                factory.recycle_arrow(tt_arrow);
            }
        }
        if (Input.GetKeyDown(KeyCode.L))//按L换弹
        {
            LoadArrow();
            mes = "箭已装满";
        }
    }
    public void LoadArrow()
    {
        arrow_num = 10;
        while (arrows.Count != 0)
        {
            factory.recycle_arrow(arrows[0]);
            arrows.RemoveAt(0);//清空箭队列
        }
        for(int i = 0; i < 10; i++)
        {
            GameObject arrow = factory.get_arrow();
            arrows.Add(arrow);
        }
    }
    public void Shoot()//射箭
    {
        if (arrows.Count > 0)//判断是否有箭
        {
            if (!gameObject.GetComponent<BowController>().can_shoot && (Input.GetMouseButtonDown(0) || Input.GetButtonDown("Fire2")))
            {
                mes = "按1去静态靶场,按2去动态靶场";
            }
            else
            {
                aniShoot();
            }
        }
        else
        {
            mes = "请按L装箭";
        }
    }
    public void Hit_Ground()//箭是否射中地面
    {
        RaycastHit hit;
        if (arrow != null && Physics.Raycast(arrow.transform.position, Vector3.down, out hit, 2f))
        {
            // 如果箭与地面相交
            if (hit.collider.gameObject.name == "Terrain")
            {
                arrow.tag = "ground";
                arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);//设置箭速度为0
                arrow.GetComponent<Rigidbody>().isKinematic = true;//开启运动学
            }
        }
    }
    public void aniShoot()//射箭动画
    {
        if (Input.GetMouseButtonDown(0) && state == 0)//鼠标左键按下
        {
            mes = "";
            transform.GetChild(4).gameObject.SetActive(true);//激活
            is_longpressing = true;
            ani.SetTrigger("pull");
            press_time = Time.time;
            state = 1;
            play_sound_pull();//拉弓音效
        }
        if (Input.GetMouseButtonUp(0) && state == 1)//鼠标左键抬起
        {
            is_longpressing = false;
            float duration = Time.time - press_time;
            if (duration < longpress_duration)
            {
                power = duration / 2;
            }
            else
            {
                power = 1.0f;//满弓
            }
            ani.SetFloat("power", power);
            ani.SetTrigger("hold");
            state = 2;
        }
        if (is_longpressing && Time.time - press_time > longpress_duration)//长按鼠标左键未抬起
        {
            is_longpressing = false;
            power = 1.0f;//按常识也是拉满状态
            ani.SetFloat("power", power);
            ani.SetTrigger("hold");
        }
        if (Input.GetButtonDown("Fire2") && state == 2)//鼠标右键按下
        {
            transform.GetChild(4).gameObject.SetActive(false);//不可见
            ani.SetTrigger("shoot");
            arrow = arrows[0];
            arrow.SetActive(true);
            flyed.Add(arrow);
            arrows.RemoveAt(0);
            arrow_manager.ArrowShoot(arrow, main_camera.transform.forward, power);
            ani.SetFloat("power", 1.0f);//恢复
            arrow_num = arrow_num - 1;
            state = 0;
            play_sound_shoot();//射箭音效
        }
    }

    void play_sound_pull()//拉弓
    {
        audio_source.clip = pull_sound;//设置音频片段
        audio_source.Play();
    }
    void play_sound_shoot()//射箭
    {
        audio_source.clip = shoot_sound;//设置音频片段
        audio_source.Play();
    }

    public int get_arrow_num()
    {
        return arrow_num;//返回剩余箭数量
    }

    public int get_score()
    {
        return recorder.score;//返回当前分数
    }

    public string get_message()
    {
        return mes;//返回提示信息
    }
}

3.3 视图(Views)

3.3.1 IUserAction

IUserAction是一个用户交互动作的接口,定义了用户在这个界面上可以干什么。对于第一人称射箭小游戏来说,用户可以获取得分信息,获取背包中剩余的箭的数量,获取相关信息。

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

public interface IUserAction
{
    int get_score();
    int get_arrow_num();
    string get_message();
}

3.3.2 UserGUI

UserGUI是用户交互界面组件。这里显示了玩家的得分,背包剩余箭数,同时还有一些提示信息。需要注意的是,游戏画面里有一个红色的瞄准镜,这个其实是小写字母o,由于Update()函数一直更新屏幕高度和宽度,因此小写字母o可以一直稳定在屏幕正中央,充当瞄准镜。

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

public class UserGUI : MonoBehaviour
{
    private IUserAction action;
    GUIStyle style1 = new GUIStyle();
    GUIStyle style2 = new GUIStyle();
    public string mes = "";// 提示信息
    int screen_width, screen_height;
    // Start is called before the first frame update
    void Start()
    {
        action = this.transform.gameObject.GetComponent<IUserAction>();

        style1.normal.textColor = Color.white;
        style1.fontSize = 30;

        style2.normal.textColor = Color.red;
        style2.fontSize = 40;
    }

    // Update is called once per frame
    void Update()
    {
        screen_width = Screen.width;
        screen_height = Screen.height;
    }

    private void OnGUI()
    {
        //显示各种信息
        GUI.Label(new Rect(20, 10, 150, 50), "得分: ", style1);
        GUI.Label(new Rect(100, 10, 150, 50), action.get_score().ToString(), style2);

        GUI.Label(new Rect(20, 50, 150, 50), "剩余箭数: ", style1);
        GUI.Label(new Rect(200, 50, 150, 50), action.get_arrow_num().ToString(), style2);

        GUI.Label(new Rect(150, 100, 150, 50), action.get_message(), style1);

        GUI.Label(new Rect(screen_width / 2 + 10, screen_height / 2, 150, 50), "o", style2);
    }
}

四、游戏效果

第一人称射箭小游戏演示视频

第一人称射箭小游戏演示视频

五、代码链接

通过网盘分享的文件:Assets.rar
链接: https://pan.baidu.com/s/1XwEa47OAaWpEzvBp8srG1g

提取码: 4d6j

解压后新建一个unity3d的项目,将所有项目放到Assets文件夹下,点击Scene文件夹下的场景文件即可运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值