一、游戏玩法与介绍
第一人称射箭小游戏是基于unity3d编写的小游戏。玩家视角为一个弓弩,通过鼠标操控弓弩的视角方向,同时通过键盘的上下左右键或者“wasd”键来控制弓弩的移动方向。地图中有一个初始位置和两个靶场,玩家一开始降临在初始位置,按下1键进入静态靶场,里面有两种靶子,一大一小,共8个。按下2键进入动态靶场,里面同样有两种靶子,一种运动速度较快、变化多,一种运动速度较慢,且为直线运动。
游戏需求:
- 地形:使用地形组件,上面有山、路、草、树;
- 天空盒:使用天空盒,天空可随特定按键切换天空盒;
- 固定靶:使用静态物体,有一个以上固定的靶标;(注:射中后状态不会变化)
- 运动靶:使用动画运动,有一个以上运动靶标,运动轨迹,速度使用动画控制;(注:射中后有效果或自然落下)
- 射击位:地图上标记了两个射击区域,仅在射击区域可以拉弓射击;
- 摄像机:有一个瞄准镜,使得游戏更加易于操控;
- 声音:使用声音组件,播放背景音 与 箭射出的声效;
- 运动与物理与动画
- 游走:使用第一人称组件,玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 射击效果:使用 物理引擎 或 动画 或 粒子,运动靶被射中后产生适当效果。
- 碰撞与计分:使用 计分类 管理规则,在射击位射中靶标得相应分数,规则自定;
- 驽弓动画:使用 动画机 与 动画融合, 实现十字驽蓄力半拉弓,然后 hold,择机 shoot;
计分规则如下(从左到右编号):
固定靶:
1 | 2 | 3 | 4 | |
大(近) | 5 | 4 | 3 | 2 |
小(远) | 10 | 11 | 12 | 13 |
运动靶:
慢速靶 | 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文件夹下的场景文件即可运行。