目录
一、简介
这是一个第一人称射击游戏。你将以第一人称视角操控弓弩,移动到指定的射击位,使用弩箭射击标靶从而得到相应的分数。
1. 项目要求:
- 地形:使用地形组件,上面有草、树;
- 天空盒:使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶:有一个以上固定的靶标;
- 运动靶:有一个以上运动靶标,运动轨迹,速度使用动画控制;
- 射击位:地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
- 驽弓动画:支持蓄力半拉弓,然后 hold,择机 shoot;
- 游走:玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 碰撞与计分:在射击位,射中靶标的相应分数,规则自定;
2. 游戏视频
Unity--第一人称射箭游戏
Unity--第一人称射箭游戏_演示 (bilibili.com)https://www.bilibili.com/video/BV1xw41187ig/
二、游戏设计
1. 游戏对象:
(1)地形:
使用地形组件,且上面有树和草。
(2)天空盒
天空盒会随着玩家的位置跳转(按下1、2、3)而改变。
(3)固定靶
小靶子target和大靶子big_larget做成预制,属性如下所示:
巨型靶子large_target也做成了预制,但是其每一个环都有自己的碰撞体积,在后续的代码中会为每一个环添加碰撞检测组件:
(4)运动靶
包含了target_move和target_quick两种运动靶,都包含自己的动作以及相应的动作控制器。
(5)箭矢
在箭头处包含一个盒型碰撞体。
需要为其添加三个tag,分别是ground、arrow和onTarget。
(6)围墙
围墙用于划分不同的区域。其碰撞体积大于物体的真实体积,可以防止箭矢射到其他区域(即所谓的空气墙)。
(7)文本
用于显示游戏介绍信息和提示信息。
(8)弓弩(玩家)
为了方便控制视角的转换,这里将弓弩作为一个方块(或其他3D模型)的子对象,该方块可以当作玩家。同时,将主相机作为弓弩的子对象,跟随弓弩一起移动。由于全局只有一个弓弩对象,因此这里没有将其作为预制。
弓弩对象需要添加动画控制器,用来演示射箭的动作。同时,还需要将弓弩控制器脚本和场景控制器脚本都挂载在弓弩对象上。还需要添加盒装碰撞体来防止与其他游戏对象发生碰撞。
2. 文件组织形式
Assets目录下有如下的文件及文件夹:
Fantasy Skybox FREE是在unity store下载的天空盒,RyuGiKen是在unity store下载的弓弩和箭矢,Tree9是在unity store下载的树的模型,TextMesh Pro是添加Text组件时下载的依赖包。
Animations目录下是创建的动画效果和动画控制器。包含了移动靶子的两种动画以及它们相应的动画控制器,以及一个弩射箭的动画控制器:
Resources目录包含了两个子目录Materials和Prefabs。其中Materials目录包含了用于制作靶子预制的材质、墙的材质和天空盒的材质;Prefabs目录包含了箭矢的预制以及多种不同的靶子的预制。如下图所示:
Scenes目录是当前的场景:
Scripts目录下包含了游戏的脚本。其中根据MVC模式和动作分离的规则,将脚本分成了三个子目录:Actions目录下包含动作以及动作控制器的脚本;Controllers目录下包含了各个控制器脚本;Views目录下包括了与用户交互相关的脚本。
3. 动画控制器
(1)弓弩射箭的动画控制器
(2)运动靶的动画控制器(两个)
三、代码介绍
1. 动作部分Actions
(1)ISSCallback 回调函数接口
动作分离模式的模板函数,详情见前几篇博客,不再赘述。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType:int {Started, Completed} // 枚举动作事件类型
public interface ISSCallback
{
//回调函数
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
(2)SSAction 动作基类
动作分离的模板函数,不再赘述。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 模板代码,不用管
// 动作的基类
public class SSAction : ScriptableObject
{
public bool enable = true; //是否正在进行此动作
public bool destroy = false; //是否需要被销毁
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSCallback callback; //动作完成后的消息通知者
protected SSAction() { }
//子类可以使用下面这两个函数
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
public virtual void FixedUpdate()
{
throw new System.NotImplementedException();
}
}
(3)SSActionManager 动作管理器的基类
模板函数,不再赘述。
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>();
protected void Start(){
}
// Update每一帧都执行,执行的帧率取决于电脑性能
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());
}
//动作不需要销毁则更新
else if(ac.enable){
ac.Update();
}
}
//销毁动作
foreach (int key in waitingDelete){
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
waitingDelete.Clear();
}
// FixedUpdate每秒调用50次,适合用来写物理引擎相关的代码
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();
}
//新增一个动作,运行该动作
public void RunAction(GameObject gameobject, SSAction action, ISSCallback manager)
{
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
//回调新的动作类型
public void SSActionEvent(SSAction source, int param = 0,GameObject arrow = null)
{
//执行完飞行动作
}
}
(4)CCShootAction 射箭动作
该脚本是射箭的具体动作,挂载在箭矢上,实现于动作基类SSAction。
Start()函数用于初始化,由于箭没射出时是跟随弓移动的,因此需要解除与弓的绑定;然后,设置箭使用重力;为了符合现实规律,这里设置箭的初始速度为0,然后为其添加一个特定方向的冲量;最后关闭箭的运动学控制,使其受物理引擎的控制从而能够运动。
GetSSAction()函数用于获得一个动作。该函数以冲量的方向impulseDirection和射箭的力power作为参数,然后创建一个动作实例并为其添加指定方向和大小的冲量,最后返回该动作实例。
FixdUpdate()函数用来不断判断箭矢是否飞出场景、落在地上或者射在靶子上。如果满足上述的一种,则说明该动作完成了,销毁该动作即可。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 射箭动作
public class CCShootAction : SSAction
{
private Vector3 pulseForce; //射箭提供的冲量
private CCShootAction(){}
public static CCShootAction GetSSAction(Vector3 impulseDirection,float power) //获得一个射箭动作实例
{
CCShootAction shootarrow = CreateInstance<CCShootAction>();
shootarrow.pulseForce = impulseDirection.normalized * power; // 射箭提供的冲量(方向*力度)
return shootarrow;
}
public override void Start()
{
gameobject.transform.parent = null; // 摆脱跟随弓移动
gameobject.GetComponent<Rigidbody>().useGravity = true; // 发射的时候才开启重力
gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero; // 初速度为0
gameobject.GetComponent<Rigidbody>().AddForce(pulseForce,ForceMode.Impulse); //添加冲量
//关闭运动学控制
gameobject.GetComponent<Rigidbody>().isKinematic = false; // 此时箭的运动由物理引擎控制
}
public override void Update(){
}
public override void FixedUpdate(){ // 判断箭是否飞出场景、落在地上或者射中靶子
if(!gameobject || this.gameobject.tag == "ground" || this.gameobject.tag == "onTarget" )
{
this.destroy = true; // 摧毁动作
this.callback.SSActionEvent(this);
}
}
}
(5)CCShootManager 动作管理器
该脚本是射箭动作的管理器,实现于动作管理器基类SSActionManager。
ArrowShoot()函数以弓箭对象、冲量方向和力的大小为参数。然后,使用冲量方向和力创建一个射箭的动作。最后调用父类的RunAction()函数运行该动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 射箭动作的管理器
public class CCShootManager : SSActionManager, ISSCallback
{
//箭飞行的动作
private CCShootAction shoot;
protected new void Start(){
}
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null) {
}
//箭飞行
public void ArrowShoot(GameObject arrow, Vector3 impulseDirection, float power) // 游戏对象、力的方向、力的大小
{
shoot = CCShootAction.GetSSAction(impulseDirection, power); //实例化一个射箭动作。
RunAction(arrow, shoot, this); //调用SSActionmanager的方法运行动作。
}
}
2. Controllers
(1)Singleton 场景单实例
场景单实例的模板,前几篇博客讲过,不再赘述。
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;
}
}
}
(2)SSDirector 导演类
导演模板,不再赘述。
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)ScoreRecorder 计分器
用于记录射箭的总得分。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 计分器类
public class ScoreRecorder : MonoBehaviour
{
public int score;
void Start()
{
score = 0;
}
public void RecordScore(int ringscore)
{
score += ringscore; //增加新的值
}
}
(4)ArrowFactory 箭矢工厂
工厂模式的脚本,用于生产箭矢和回收箭矢。根据该游戏的需求,这里对工厂模式进行了修改,取消了对象池,将其抽象成加载预制和删除游戏对象的简单工厂。
GetArrow()函数。该函数从Prefabs目录中获取箭矢的预制。由于采用了动画和动作分开的设计方法,弓弩子对象有一个用于动画演示的箭矢,因此可以把动画箭矢的位置作为动作箭矢的位置,并且设置动作箭矢的旋转角度与弓弩一致。然后将动作箭矢作为弓弩的子对象,以便动作箭矢和弓弩一起移动。为了防止动作箭矢和弓弩产生碰撞而产生错误,这里可以先把动作箭矢隐藏起来。最后,把该动作箭矢返回即可。
RecycleArrow()函数。该函数以需要回收的箭矢为输入参数,然后将箭矢隐藏起来,并立即删除该箭矢对象即可。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 箭的工厂,用于创建箭和回收箭
public class ArrowFactory : MonoBehaviour
{
public ArrowController arrowCtrl;
public GameObject arrow;
void Start()
{
arrowCtrl = (ArrowController)SSDirector.getInstance().currentSceneController;
}
void Update(){
}
public GameObject GetArrow(){ // 获取空闲的箭
arrow = GameObject.Instantiate(Resources.Load("Prefabs/Arrow", typeof(GameObject))) as GameObject;
//得到弓箭上搭箭的位置
Transform bow_mid = arrowCtrl.bow.transform.GetChild(4); // 获得箭应该放置的位置
arrow.transform.position = bow_mid.transform.position; //将箭的位置设置为弓中间的位置
arrow.transform.rotation = arrowCtrl.bow.transform.rotation; // 将箭的旋转角度设置为弓的旋转角度
arrow.transform.parent = arrowCtrl.bow.transform; //箭随弓的位置变化
arrow.gameObject.SetActive(false);
return arrow;
}
public void RecycleArrow(GameObject arrow) // 回收箭
{
arrow.SetActive(false);
DestroyImmediate(arrow);
}
}
(5)TargetController 靶子控制器
该脚本作为组件挂载在每一个靶子对象上。该类具有变量RingScore用来表示这个靶子对象的分值(对于那个巨型靶子而言,就是每一个环的分值)。
该脚本具有碰撞检测函数。当发生碰撞时,先获取碰撞的对象,检测其是否为空以及是否为箭矢对象。当碰撞对象是箭矢时,为了能够将箭矢保存在靶子上,这里设置箭矢的速度为0,然后关闭物理引擎的控制,将旋转角度恢复,并将箭矢作为靶子的子对象,将箭矢的tag标记为“OnTarget”。最后使用计分器记录下该靶子对应的分值。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 靶子的控制器
public class TargetController : MonoBehaviour // 用于处理靶子的碰撞
{
public int RingScore = 0; //当前靶子或环的分值
public ScoreRecorder sc_recorder;
void Start(){
sc_recorder = Singleton<ScoreRecorder>.Instance;
}
void Update(){
}
void OnCollisionEnter(Collision collision) // 检测碰撞
{
Transform arrow = collision.gameObject.transform; // 得到箭身
if(arrow == null) return;
if(arrow.tag == "arrow"){
//将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
//使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
arrow.transform.rotation = Quaternion.Euler(0, 0, 0); // 使箭的旋转角度为0
arrow.transform.parent = this.transform; // 将箭和靶子绑定
sc_recorder.RecordScore(RingScore); //计分
arrow.tag = "onTarget"; //标记箭为中靶
}
}
}
(6)BowController 弓弩控制器
该脚本挂载在弓弩对象上,用于控制弩的移动、视角的旋转、以及将玩家传送到指定的射击位置。
Move()函数读取方向键的输入并以此移动游戏对象。由于弓弩是游戏玩家(这里用方块表示)的一个子对象,因此位置移动时根据方块进行的,需要调用transform.parent。
SetCursorToCentre()函数用来将鼠标移动到屏幕中心并隐藏。
View()函数用于控制视野。其中分为水平转动视角和垂直转动视角。水平方向的旋转是针对弓弩的父对象(玩家)进行的,这样弓弩会随着玩家一起水平旋转;垂直方向的旋转是针对弓弩进行的,这样当弓弩进行俯仰操作的时候玩家不会受影响。
transport()函数用于将玩家传送到指定的射击位置。当玩家按下按键“1、2、3”时,会被传送到不同的射箭区域,同时会改变当前的天空盒;同时由于canshoot变量的存在,只有在这些射箭区域才能射箭。按下按键“B”时会回到原来的位置(出生点)。
FixedUpdate()函数会不断执行上述的几个函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 挂载在弩上,控制弩的移动和视角的旋转、以及传送到指定的射击位置
public class BowController : MonoBehaviour
{
public float moveSpeed = 6; //速度:每秒移动6个单位长度
public float angularSpeed = 90; //角速度:每秒旋转90度
public float jumpForce = 200f; //跳跃参数
public float horizontalRotateSensitivity = 5; //水平视角灵敏度
public float verticalRotateSensitivity = 5; //垂直视角灵敏度
private float xRotation = 0f; // x旋转角度
public bool canshoot = false; // 是否可以射箭
void Start()
{
}
void FixedUpdate()
{
Move();
View();
transport();
}
void Move() // 移动
{
float v = Input.GetAxis("Vertical");
float h = Input.GetAxis("Horizontal");
transform.parent.Translate(Vector3.forward * v * Time.deltaTime * moveSpeed);
transform.parent.Translate(Vector3.right * h * Time.deltaTime * moveSpeed);
}
void SetCursorToCentre() // 锁定鼠标到屏幕中心
{
//锁定鼠标后再解锁,鼠标将自动回到屏幕中心
Cursor.lockState = CursorLockMode.Locked;
Cursor.lockState = CursorLockMode.None;
//隐藏鼠标
Cursor.visible = false;
}
void View() // 控制视角
{
SetCursorToCentre(); //锁定鼠标到屏幕中心
float mouseX = Input.GetAxis("Mouse X") * Time.deltaTime * angularSpeed* horizontalRotateSensitivity;
transform.parent.Rotate(Vector3.up * mouseX); // "人"水平旋转
float mouseY = Input.GetAxis("Mouse Y") * Time.deltaTime * angularSpeed * verticalRotateSensitivity;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, -45f, 45f); // 限制上下视角
transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f); // "人"不动,弓上下移动
}
void transport() // 传送到指定的射击位置
{
if (Input.GetKeyDown(KeyCode.Alpha1)) // 静态靶子区域
{
canshoot = true;
transform.parent.position = new Vector3(120, 1, 70);
RenderSettings.skybox = Resources.Load<Material>("Materials/SkyboxMaterial1");
}
if (Input.GetKeyDown(KeyCode.Alpha2)) // 动态靶子区域
{
canshoot = true;
transform.parent.position = new Vector3(-120, 1, -70);
RenderSettings.skybox = Resources.Load<Material>("Materials/SkyboxMaterial2");
}
if (Input.GetKeyDown(KeyCode.Alpha3)) // 静态大靶子区域
{
canshoot = true;
transform.parent.position = new Vector3(-120, 1, 70);
RenderSettings.skybox = Resources.Load<Material>("Materials/SkyboxMaterial3");
}
if (Input.GetKeyDown(KeyCode.Alpha4)) // 未完待续、、、
{
canshoot = true;
transform.parent.position = new Vector3(120, 1, -70);
RenderSettings.skybox = Resources.Load<Material>("Materials/SkyboxMaterial2");
}
if (Input.GetKeyDown(KeyCode.B)) // 回到原始位置
{
canshoot = false;
transform.parent.position = new Vector3(0, 1, -8);
}
}
}
(7)ISceneController 场景控制器接口
包含了场景控制相关的函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 场景控制器接口,与场景控制有关的函数都在这里
public interface ISceneController
{
void LoadResources(); // 加载资源
}
(8)ArrowController 主场景控制器
该脚本挂载在弓弩对象上,是整个游戏的主场景控制器,用于将动作、场景、用户交互界面关联起来。
LoadResources()用于加载各种资源。首先从弓弩的子对象处获得主相机。然后加载靶子的预制。靶子一共有5种:对于两种静态靶子,每种静态靶子加载3个对象,这些对象的位置都不一样,分数也随着距离的增加而增加,且小靶子的得分高于大靶子;对于两种动态靶子,由于动画效果有固定的位置,因此不需要设定位置信息,并且每种动态靶子只能加载一个对象;对于巨型靶子,由于其每一个环都有不同的分值,因此在加载完巨型靶子后,还需要给它的每一个环都挂载靶子控制器的组件并为它们赋值不同的分值。
LoadTarget()函数。由于上述几种靶子的加载都是同样的逻辑,因此可以抽象出一个通用的加载靶子的函数。该函数以预制的名字、分数和位置作为参数,根据这些参数从prefabs目录中获取预制并给靶子对象添加位置信息、得分信息并加载控制器组件。最后返回该靶子对象即可。
Start()函数进行各种初始化工作。如给弓弩对象添加各种的组件等,详情见代码。
Shoot()函数用于触发射击的动作。首先需要确保箭矢队列里面还有箭,否则提示玩家按“R”键装载弓箭。在有箭的基础上,如果当前处在不可射击的区域时,当玩家点击鼠标时提示玩家到特定的区域射击。当箭矢队列中有箭并且玩家处在可射击区域时,触发射击的动作并通过射击动作控制器来执行动作。
aniShoot()函数根据玩家不同的鼠标点击动作,执行蓄力、等待、射击的动画效果。为了确保动作是按照蓄力、等待、射击的顺序来完成的,这里引入了一个变量state用来表示当前的状态,只有在正确状态下按下鼠标,才会执行相应的动画效果。对于“蓄力”操作,根据玩家按下鼠标左键的时间长短,弓箭的力也会改变,直到蓄力到达2秒时到达最大的力;不同大小的力量,对应的动画的效果和射箭的力度也会不同。当处于等待状态并按下鼠标右键时,执行“射击”状态,在播放动画的同时,从箭矢队列中取出一支箭,将其加入已发射flyed队列中,并通过射击动作管理器为这支箭执行射击动作。至此就完成了箭矢的射击动作和动画效果。
LoadArrow()函数用于获得箭矢(相当于fps游戏中的换弹)。当调用该函数后,首先清空原来的箭矢队列,然后通过箭矢工厂获得10支箭并添加到箭矢队列中。等到需要使用的时候,就从队列中取出箭矢即可。
HitGround()函数通过射线检测的方法检测箭矢是否射中地面。如果箭矢和地面的距离小于设定的阈值(如代码中的2f)时,则认为箭矢碰撞到了地面,将箭矢的tag标记为ground,速度设置为0并取消物理引擎的控制。
GetScore()函数用来获取计分器的得分;GetArrowNum()函数用来获取剩余的箭矢的数量;GetMessage()函数用来获取需要显示在屏幕上的信息。
Update()函数用于不断调用Shoot()函数来执行射击、调用HitGround()函数来检测当前飞出去的箭是否碰撞到地面。同时不断检测已经射出去的箭的队列flyed,当该队列的数量大于10时、或者当前遍历到的箭矢的y轴值小于-10(可以认为已经飞出了地图外,因为地图的高度y是0)时,删除遍历到的箭以释放空间。(也就是说,可以同时存在场上的、射出去的箭矢的数量最多为10支,这样设计可以释放空间、减少内存压力)。同时,当玩家按下“R”键换弹时,调用LoadArrow()函数获得箭矢,更新提示信息。
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 ScoreRecorder recorder; // 计分对象
public GameObject bow; // 弓弩
public GameObject target1, target2, target3, target4, large_target; // 四种不同的靶子和一个巨型靶子
public GameObject arrow; // 当前射出的箭
public string message = ""; // 用于显示的信息
private int arrow_num = 0; // 装载的箭的数量
public Animator ani; // 动画控制器
private List<GameObject> arrows = new List<GameObject>(); // 箭的队列
private List<GameObject> flyed = new List<GameObject>(); // 飞出去的箭的队列
public float longPressDuration = 2.0f; // 设置长按持续时间
private bool isLongPressing = false; // 是否正在长按
private float pressTime = 0f; // 长按的时间
public int state = 0; // 0-普通状态,1-拉弓状态,2-蓄力状态
public float power = 0f; // 拉弓的力度
//加载箭和靶子
public void LoadResources()
{
main_camera = transform.GetChild(5).gameObject; // 获取摄像机
bow = this.gameObject;
// 加载靶子
for(int i = 0; i < 3; ++i) // 3 * 2 = 6个靶子
{
target1 = LoadTarget("target", 10 + i, new Vector3(100+20*i, 20, 140+5*i)); // 静态小靶子
target2 = LoadTarget("big_target", 5-i, new Vector3(100+20*i, 10, 140-5*i)); // 静态大靶子
}
target3 = LoadTarget("target_move", 15, new Vector3(0,0,0)); // 慢速移动靶子, 使用动画,自带位置
target4 = LoadTarget("target_quick", 20, new Vector3(0,0,0)); // 快速移动靶子,使用动画,自带位置
large_target = LoadTarget("large_target", 6, new Vector3(-100, 25, 180)); // 静态大靶子
for(int i = 0; i < 4; ++i) // 给子对象添加TargetController组件
{
Transform child = large_target.transform.GetChild(i);
child.gameObject.AddComponent<TargetController>();
child.gameObject.GetComponent<TargetController>().RingScore = 7 + i; // 不同的环数分数不同
}
}
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>(); // 给靶子添加TargetController组件
target.GetComponent<TargetController>().RingScore = score;
return target;
}
//进行资源加载
void Start()
{
arrow = null;
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
director.currentSceneController.LoadResources();
gameObject.AddComponent<ArrowFactory>();
gameObject.AddComponent<ScoreRecorder>();
gameObject.AddComponent<UserGUI>();
gameObject.AddComponent<BowController>();
factory = Singleton<ArrowFactory>.Instance;
recorder = Singleton<ScoreRecorder>.Instance;
arrow_manager = this.gameObject.AddComponent<CCShootManager>() as CCShootManager;
ani = GetComponent<Animator>();
}
void Update()
{
Shoot(); // 射击
HitGround(); // 检测箭是否射中地面
for(int i = 0; i < flyed.Count; ++i) // 用于维护已经射出去的箭的队列
{
GameObject t_arrow = flyed[i];
if(flyed.Count > 10 || t_arrow.transform.position.y < -10) // 箭的数量大于10或者箭的位置低于-10
{ // 删除箭
flyed.RemoveAt(i);
factory.RecycleArrow(t_arrow);
}
}
if(Input.GetKeyDown(KeyCode.R)) // 按R换弹
{ //从工厂得到箭
LoadArrow();
message = "弩箭已填充完毕";
}
}
public void Shoot() // 射击
{
if(arrows.Count > 0){ // 确保有箭
if(!gameObject.GetComponent<BowController>().canshoot && (Input.GetMouseButtonDown(0) || Input.GetButtonDown("Fire2"))){
message = "请按1、2、3到指定地点射击";
}
else{
aniShoot();
}
}
else{
message = "请按R键以装载弓箭";
}
}
public void aniShoot(){ // 射击的动画
if (Input.GetMouseButtonDown(0) && state==0) // 监测鼠标左键按下
{
message = "";
transform.GetChild(4).gameObject.SetActive(true); // 设置动作中的箭可见
isLongPressing = true;
ani.SetTrigger("pull");
pressTime = Time.time;
state = 1;
}
if (Input.GetMouseButtonUp(0) && state==1) // 监测鼠标左键抬起
{
isLongPressing = false;
float duration = Time.time - pressTime;
if (duration < longPressDuration){ // 执行普通点击操作
power = duration/2;
}
else{ // 拉满了
power = 1.0f;
}
ani.SetFloat("power", power);
ani.SetTrigger("hold");
state = 2;
}
if (isLongPressing && Time.time - pressTime > longPressDuration) // 长按但是未抬起,且持续时间超过设定值
{
// 长按操作
isLongPressing = false;
power = 1.0f;
ani.SetFloat("power", power);
ani.SetTrigger("hold");
}
if (Input.GetButtonDown("Fire2") && state==2){ // 鼠标右键,攻击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 -= 1;
state = 0;
}
}
public void LoadArrow(){ // 获得10支箭
arrow_num = 10;
while(arrows.Count!=0){ // 清空队列
factory.RecycleArrow(arrows[0]);
arrows.RemoveAt(0);
}
for(int i=0;i<10;i++){
GameObject arrow = factory.GetArrow();
arrows.Add(arrow);
}
}
public void HitGround(){ // 检测箭是否射中地面
RaycastHit hit;
if (arrow!=null && Physics.Raycast(arrow.transform.position, Vector3.down, out hit, 2f))
{
// 如果射线与地面相交
if (hit.collider.gameObject.name == "Terrain")
{
arrow.tag = "ground";
//将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
//使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
}
}
}
//返回当前分数
public int GetScore(){
return recorder.score;
}
//得到剩余的箭的数量
public int GetArrowNum(){
return arrow_num;
}
// 显示的信息
public string GetMessage(){
return message;
}
}
3. Views
(1)IUserAction 用户交互的接口
包含了与用户交互界面相关的函数。这些函数在ArrowController中实现了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 与用户交互的接口
public interface IUserAction
{
int GetScore();
int GetArrowNum();
string GetMessage();
}
(2)UserGUI 用户交互界面
用于显示与用户交互相关的信息。
Start()函数用于初始化字体信息,并获得用户交互接口的实例。
Update()函数用于实时获取屏幕的宽度和高度。
OnGUI()用于显示玩家的得分信息、剩余箭矢数、以及用于提示玩家的信息。同时这里还在屏幕中央加了一个圆圈,用来作为射击的准星,提高玩家游戏体验。
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 message = ""; // 显示的信息
int screenWidth, screenHeight;
void Start()
{
action = this.transform.gameObject.GetComponent<IUserAction>();
style1.normal.textColor = Color.red;
style1.fontSize = 30;
style2.normal.textColor = Color.yellow;
style2.fontSize = 40;
}
void Update()
{
screenWidth = Screen.width;
screenHeight = Screen.height;
}
private void OnGUI()
{
//显示各种信息
GUI.Label(new Rect(20,10,150,50),"得分: ",style1);
GUI.Label(new Rect(100,10,150,50),action.GetScore().ToString(),style2);
GUI.Label(new Rect(20,50,150,50),"剩余箭矢数: ",style1);
GUI.Label(new Rect(200,50,150,50),action.GetArrowNum().ToString(),style2);
GUI.Label(new Rect(150,100,150,50),action.GetMessage(),style1);
GUI.Label(new Rect(screenWidth/2+10,screenHeight/2,150,50),"o",style2);
}
}
四、代码链接
Unity-FPS: 这是一个第一人称射击游戏。你将以第一人称视角操控弓弩,移动到指定的射击位,使用弩箭射击标靶从而得到相应的分数。