一、前言
本文将介绍如何使用 Unity 实现一个简单的第一人称射箭游戏,本项目采用通过 MVC 架构并且实现了动作分离,使代码结构清晰、功能模块化。本文将详细阐述游戏的背景与规则、项目模块结构,以及各模块中类和接口的主要功能和交互。
项目演示和源代码URL如下:
视频演示:unity小游戏-fps演示视频_哔哩哔哩bilibili_演示
源代码:lab-fps/Assets · 22331077/3D游戏编程与设计 - 码云 - 开源中国
二、游戏介绍
本项目实现了一个第一人称射击游戏,玩家以第一视角操纵弓弩,在空间内移动,并在三个靶场对应的点位进行不同类型的射箭打靶,获得分数。
1.项目要求:
- 游戏场景
- 地形:使用地形组件,上面有山、路、草、树;(可使用第三方资源改造)
- 天空盒:使用天空盒,天空可随 玩家位置 或 时间变化 或 按特定按键切换天空盒;
- 固定靶:使用静态物体,有一个以上固定的靶标;(注:射中后状态不会变化)
- 运动靶:使用动画运动,有一个以上运动靶标,运动轨迹,速度使用动画控制;(注:射中后需要有效果或自然落下)
- 射击位:地图上应标记若干射击位,仅在射击位附近或区域可以拉弓射击,每个位置有 n 次机会;
- 摄像机:使用多摄像机,制作 鸟瞰图 或 瞄准镜图 使得游戏更加易于操控;
- 声音:使用声音组件,播放背景音 与 箭射出的声效;
- 运动与物理与动画
- 游走:使用第一人称组件,玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
- 射击效果:使用 物理引擎 或 动画 或 粒子,运动靶被射中后产生适当效果。
- 碰撞与计分:使用 计分类 管理规则,在射击位射中靶标得相应分数,规则自定;(注:应具有现场修改游戏规则能力)
- 驽弓动画:使用 动画机 与 动画融合, 实现十字驽蓄力半拉弓,然后 hold,择机 shoot;
三、游戏设计
1.游戏对象
(1)地形
使用Unity的Terrain组件,在上面添加加载树的预制和草的预制,从而形成人物活动的主要区域
地形中存在两个cube将地图分为三个区域,每个区域存在一个射击点位和对应的靶场。
(2)天空盒
使用store中的天空盒资源Fantasy Skybox FREE,游戏中玩家可点击C键切换天空盒。
(3)玩家(弓弩)
为方便视角控制,将弓弩作为一个Capsule的子对象,并绑定在MainCamera上。此外还有一个Slider子对象用于显示拉弓时的蓄力条。
(4)固定靶
固定靶分为两种,一种是不带环数的普通靶,该靶存在三种大小,射中后根据靶大小获得分数,在第一射击点位对应的靶场中使用;另一种时带环数的大型靶子,射中后根据环数获得分数,在第三射击点位对应的靶场中使用。
(5)运动靶
存在两种运动靶,由第一种固定靶添加不同的动画控制器制作而成,对应两种运动轨迹,在第二射击点位对应的靶场中使用。
(6)射击位
游戏中存在三个射击位,对应三种靶场,玩家站在射击点位内才可以进行射击。游戏中玩家点击(Alpha)1、2、3可传送到对应的射击点位附近。每个射击位拥有一定的可射击次数,相互之间不影响
(7)摄像机
本项目设置了两个摄像机,分别为实现第一人称视角的主摄像机Main Camera和实现鸟瞰图的副摄像机SecCamera。通过RawImage组件(AerialView)映射SecCamera实现鸟瞰图。
(8)声音
游戏中存在三种音效:背景音乐、拉弓声、射箭声,通过Audio Source组件实现声音的播放。
背景音乐通过对象main camere上的Audio Source组件实现播放,拉弓声和射箭声通过对象Crossbow上的ShootController脚本控制播放。
2.游戏效果
(1)游走
游戏中玩家可通过移动键(箭头或wasd)进行前后左右的移动,也可通过点击(Alpha)1、2、3传送到对应的射击点位旁边。此外,玩家可通过鼠标移动来转动视角。
(2)射击效果
采用物理引擎实现箭矢飞行动作。箭矢发射后,会为其附加相应的冲量,由物理引擎控制其飞行过程,箭矢碰撞到靶子后会停留在靶子上。
(3)碰撞与计分
靶子检测到箭矢碰撞后,将会根据靶子的情况进行相应的加分。游戏中会显示总得分和各个射击点位的得分。
(4)弓弩动画
使用RyuGiKen资源包中的弩和制作好的动画来完成弩的动作。动作包含pull(blend tree)和hold(blend tree0)两个动作。
四、代码架构
1.CCAction模块
回调函数ISSActionCallback、动作基类SSAction、动作基类管理器SSActionManager均为模版代码,此处不再作介绍。
(1)IActionManager(动作管理器接口)
IActionManager
接口定义了一个箭矢射击方法 ArrowShoot
,用于控制游戏对象(箭)的射击行为,包括方向和力度。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IActionManager
{
// 游戏对象、力的方向、力的大小
void ArrowShoot(GameObject arrow, Vector3 Direction, float power);
}
(2)CCActionManager(动作管理器)
CCActionManager实现了IActionManager定义的方法,并在动作完成时进行回调。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback, IActionManager
{
private CCMoveToAction action;
//箭飞行
public void ArrowShoot(GameObject arrow, Vector3 Direction, float power)
{
action = CCMoveToAction.GetSSAction(Direction, power); //实例化一个射箭动作。
RunAction(arrow, action, this); //调用SSActionmanager的方法运行动作。
}
// 回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Completed,
int intParam = 0,
string strParam = null,
Object objectParam = null){}
}
(3)CCMoveToAction(动作类)
CCMoveToAction主要负责实现箭矢的飞行逻辑,包括施加力让箭射出,以及在物理引擎控制下对箭的运动状态进行更新,并在箭矢飞出场景、落在地上或者射中靶子时销毁动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCMoveToAction : SSAction
{
private Vector3 pulseForce; //射箭提供的冲量
public static CCMoveToAction GetSSAction(Vector3 Direction,float power)
{
CCMoveToAction arrow = CreateInstance<CCMoveToAction>();
arrow.pulseForce = Direction.normalized * power;
return arrow;
}
public override void Start()
{
gameobject.transform.parent = null; // 摆脱跟随弓移动
// 获取箭矢的 Rigidbody 组件,用于处理物理运动
var rigidbody = gameobject.GetComponent<Rigidbody>();
rigidbody.useGravity = true;//发射的时候才开启重力
rigidbody.velocity = Vector3.zero; // 初速度为0
rigidbody.AddForce(pulseForce,ForceMode.Impulse);//添加冲量
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);
}
}
}
2.Controllers模块
导演类SSDirector、场景单实例Singleton、场景控制器接口ISceneController均为模版代码,此处不再介绍。
(1)射击点检测ReadyController
ReadyController挂在玩家Player对象上,用于判断玩家是否到达指定射击位置,并设置相关的弓箭准备状态。
功能实现:
- 检测玩家是否进入或离开射击点(
spot
)。 - 进入射击点,则:
- 启用射击功能。
- 设置射击点剩余次数。
- 离开射击点后,禁用射击功能。
using UnityEngine;
//挂在player上,用于检测是否到达射击点
public class ReadyController : MonoBehaviour
{
public ShootController CrossBow;// 弓对象
private bool atSpot = false;// 是否到达射击点
private SpotController spot;// 射击位controller
void Start()
{
CrossBow = GetComponentInChildren<ShootController>();
}
void Update()
{
//如果进入了射击位置
if (atSpot)
{
//屏幕左上角出现对应提示
Singleton<UserGUI>.Instance.SetIsAtSpot(true);
}
else
{
Singleton<UserGUI>.Instance.SetIsAtSpot(false);
}
}
private void OnCollisionEnter(Collision collision)
{
// 与射击点位撞击
if (collision.gameObject.tag == "spot")
{
spot = collision.gameObject.GetComponentInChildren<SpotController>();
atSpot = true;
CrossBow.GetComponentInChildren<ShootController>().shootNum = spot.shootNum;
CrossBow.GetComponentInChildren<ShootController>().currentSpotController = spot;
if (spot.shootNum > 0)
{
CrossBow.GetComponentInChildren<ShootController>().readyToShoot = true;
}
}
}
private void OnCollisionExit(Collision collision)
{
if (collision.gameObject.tag == "spot")
{
CrossBow.GetComponentInChildren<ShootController>().readyToShoot = false;
atSpot = false;
}
}
}
(2)玩家移动PlayerController
PlayerController
脚本挂在主相机MainCamera对象上,实现了角色控制、视角转动、以及传送功能。
-
角色移动:
- 使用
CharacterController
来管理角色的移动,通过Input.GetAxis
获取玩家输入。 - 处理重力效果以模拟自然下落。
- 使用
-
视角转动:
- 使用鼠标输入来控制视角,包含水平方向的转动和垂直方向的俯仰角控制。
- 通过
Mathf.Clamp
限制垂直视角范围,避免角色过度仰望或俯视。
-
传送功能:
- 设置按键
Alpha1
至Alpha3
实现快速传送到不同的射击位置。 - 使用
KeyCode.B
返回原始位置。 - 使用禁用和启用
CharacterController
避免传送过程中出现碰撞问题。
- 设置按键
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//挂在主相机上,控制角色的移动和视角,以及传送到指定射击位置
public class PlayerController : MonoBehaviour{
public CharacterController controller;//用于控制角色移动的组件,需要在unity中编辑
public Transform playerBody;//需要在unity中编辑
public float speed = 12f;//移动速度
private float gravity = 9.8f;
public float mouseSensitivity = 400f;//鼠标灵敏度(视角转换速度)
float xRotation = 0f;
Vector3 move;
void Start()
{
Cursor.lockState = CursorLockMode.Locked;//锁定鼠标到屏幕中心
}
void Update() {
playerMove();//角色移动、跳跃
turnView();//视角转动
transport();//传送
}
//角色移动
void playerMove(){
if(controller.isGrounded){
float x = Input.GetAxis("Horizontal");
float z = Input.GetAxis("Vertical");
move = transform.right * x + transform.forward * z;
}
move.y = move.y - gravity*Time.deltaTime;
controller.Move(move * speed * Time.deltaTime);
}
//转动视角
void turnView(){
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, -90f, 90f);// 限制上下视角
transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
playerBody.Rotate(Vector3.up * mouseX);
}
// 传送到指定的射击位置
void transport(){
// 静态靶子区域
if (Input.GetKeyDown(KeyCode.Alpha1)){
playerBody.eulerAngles = new Vector3(0, -90, 0); // 调整朝向
controller.enabled = false; // 禁用控制器避免冲突
controller.transform.position = new Vector3(-5, 0, 150);//移动到指定位置
controller.enabled = true;
}
// 动态靶子区域
if (Input.GetKeyDown(KeyCode.Alpha2))
{
playerBody.eulerAngles = new Vector3(0, 0, 0); // 调整朝向
controller.enabled = false; // 禁用控制器避免冲突
controller.transform.position = new Vector3(-135, 0, -195);//移动到指定位置
controller.enabled = true;
}
// 静态大靶子区域
if (Input.GetKeyDown(KeyCode.Alpha3))
{
playerBody.eulerAngles = new Vector3(0, 90, 0); // 调整朝向
controller.enabled = false; // 禁用控制器避免冲突
controller.transform.position = new Vector3(-15, 0, 450);//移动到指定位置
controller.enabled = true;
}
// 回到原始位置
if (Input.GetKeyDown(KeyCode.B))
{
transform.parent.position = new Vector3(350, 1, 200);
}
}
}
(3)天空盒切换SkyboxSwitcher
SkyboxSwitcher脚本挂在主相机MainCamera对象上,实现了一个简单的天空盒切换功能。旺仔可通过点击C键循环切换天空盒。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//挂在主相机上,用于切换天空盒
public class SkyboxSwitcher : MonoBehaviour
{
public Material[] skyboxMaterials; // 存储不同天空盒的材质,需要在unity中编辑
private int currentSkyboxIndex = 0; // 当前天空盒的索引
void Start()
{
RenderSettings.skybox = skyboxMaterials[currentSkyboxIndex]; // 设置初始天空盒
}
void Update()
{
// 检测按下 'C' 键
if (Input.GetKeyDown(KeyCode.C))
{
// 切换到下一个天空盒
SwitchSkybox();
}
}
void SwitchSkybox()
{
// 增加索引,确保循环切换
currentSkyboxIndex = (currentSkyboxIndex + 1) % skyboxMaterials.Length;
// 设置新的天空盒材质
RenderSettings.skybox = skyboxMaterials[currentSkyboxIndex];
}
}
(4)记分员ScoreRecorder
ScoreRecorder实现了一个简单的计分器功能,提供了一个Record(int s)函数
通过传入的参数 s
,将分数累计到当前总分 score
。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 计分器类
public class ScoreRecorder : MonoBehaviour
{
public int score;
void Start()
{
score = 0;
}
public void Record(int s)
{
score += s; //增加新的值
}
}
(5)射击点控制器SpotController
SpotController挂在三个spot上,主要功能是管理每个射击点的可射击次数、得分,并提供分数记录的函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//挂在spot上
public class SpotController : MonoBehaviour
{
public int shootNum;//可射击次数
public int spotScore;//当前射击点位获得的分数
// Start is called before the first frame update
void Start()
{
this.tag = "spot";
shootNum = 6;
}
// Update is called once per frame
void Update()
{
}
public void Record(int s)
{
spotScore += s; //增加新的值
}
}
(6)靶控制器TargetController
TargetController主要功能是处理箭与靶子的碰撞,记录分数(分别记录总分数和当前设计点分数),并更新箭的状态。
当箭与靶子发生碰撞时:将箭的速度设置为零,停止箭的移动,并将箭的父物体设置为靶子,使箭停留在靶子上。然后记录分数到 ScoreRecorder
,同时通过ShootController将分数记录到对应的SpotController上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 靶子的控制器
public class TargetController : MonoBehaviour
{
public int score = 0;
public ScoreRecorder scoreRecorder;
public ShootController shootController;//(使用其中记录的currentSpotController来记录每个射击点位的得分)
void Start(){
scoreRecorder = Singleton<ScoreRecorder>.Instance;
shootController = Singleton<ShootController>.Instance;
}
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.GetComponent<Rigidbody>().isKinematic = true;//使用运动学运动控制
arrow.transform.rotation = Quaternion.Euler(0, 0, 0);// 使箭的旋转角度为0
arrow.transform.parent = this.transform;//使弓箭留在靶上
scoreRecorder.Record(score);//计分
shootController.currentSpotController.Record(score);//计分
arrow.tag = "onTarget";
}
}
}
(7)箭矢工厂ArrowFactory
ArrowFactory提供了两个函数GetArrow()和RecycleArrow(GameObject arrow),
分别负责箭矢的获取和销毁。
GetArrow()
- 作用:实例化一个新的箭。
- 实现:
- 使用
Resources.Load()
加载预设的箭(Prefabs/Arrow
),然后实例化。 - 通过ShootController获取弓弩对象(
bow_mid
),将箭放在弓的中间位置,并将箭的位置和旋转与弓同步。 - 将箭的父物体设为弓,确保箭随着弓的移动和旋转而变化。
- 将箭对象设置为不可见(
SetActive(false)
),待拉弓时再显示
- 使用
RecycleArrow(GameObject arrow)
- 作用:回收箭对象,将其从场景中移除并销毁。
- 实现:
- 将箭设置为不可激活
- 使用
DestroyImmediate(arrow)
销毁箭对象。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 箭的工厂,用于创建箭和回收箭
public class ArrowFactory : MonoBehaviour
{
public ShootController shootCtrl;
public GameObject arrow;
void Start()
{
shootCtrl = (ShootController)SSDirector.getInstance().currentSceneController;
}
void Update(){
}
public GameObject GetArrow(){ // 获取空闲的箭
arrow = GameObject.Instantiate(Resources.Load("Prefabs/Arrow", typeof(GameObject))) as GameObject;
// 如果 shootCtrl 未初始化,则尝试初始化
if (shootCtrl == null)
{
shootCtrl = (ShootController)SSDirector.getInstance().currentSceneController;
}
//得到弓箭上搭箭的位置
Transform bow_mid = shootCtrl.bow.transform.GetChild(4);
arrow.transform.position = bow_mid.transform.position;//将箭的位置设置为弓中间的位置
arrow.transform.rotation = shootCtrl.bow.transform.rotation;// 将箭的旋转角度设置为弓的旋转角度
arrow.transform.parent = shootCtrl.bow.transform;//箭随弓的位置变化
arrow.gameObject.SetActive(false);
return arrow;
}
public void RecycleArrow(GameObject arrow) // 回收箭
{
arrow.SetActive(false);
DestroyImmediate(arrow);
}
}
(8)用户交互接口IUserAction
IUserAction接口定义了与用户交互的相关操作,分别为获取总分数GetScore、获取当前射击点位剩余射箭次数 GetArrowNum、获取当前射击点位所得分数GetSpotScore。这三个方法将在射击控制器ShootController中实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 与用户交互的接口
public interface IUserAction
{
int GetScore();
int GetArrowNum();
int GetSpotScore();
}
(9)射击控制器ShootController
ShootController在弓弩Crossbow对象上,负责资源加载以及管理弓箭射击过程中的各种逻辑。
-
资源加载:
- 加载弓箭、靶子和箭矢等资源。
- 靶子种类有静态靶子(普通、大、小)、移动靶子(慢速、快速)和一个巨型靶子。
- 每个靶子都被赋予不同的得分,其中大靶子每个环的分数独立,并且动态加载到场景中。
-
射击逻辑:
- 蓄力机制:玩家通过按住鼠标左键蓄力,每0.3秒增加蓄力值,蓄力最大为1。蓄力条会显示当前的蓄力进度,并且弓的动画也会随之变化。
- 发射箭矢:当鼠标右键按下时,弓箭会发射出去,箭矢根据蓄力的强度射出,射击后的箭矢会被添加到已发射箭矢列表,并播放射箭音效。
- 检测箭矢是否击中地面:通过射线检测箭矢是否触地,若触地,则停止箭矢的运动。
-
音效播放:
- 播放弓拉弦和箭射出的音效。
-
射击点控制:
- 在射击过程中管理当前射击点、剩余的射击次数,以及是否可以进行射击。
-
得分管理:
- 管理当前得分和射击点的得分,通过
IUserAction
接口返回得分信息。
- 管理当前得分和射击点的得分,通过
-
UI和界面:
- 使用Unity的UI系统更新蓄力条(
Powerslider
),并控制是否显示该蓄力条。
- 使用Unity的UI系统更新蓄力条(
-
复用和回收箭矢:
- 通过
ArrowFactory
和CCActionManager
管理箭矢的创建和回收。
- 通过
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
//挂在弓弩上
public class ShootController : MonoBehaviour,ISceneController, IUserAction
{
public AudioSource audioSource1;//音频源
public AudioClip pullSound;//拉弓音频
public AudioClip shootSound;//射箭音频
public CCActionManager arrowManager;
public ArrowFactory arrowFactory;
public ScoreRecorder scoreRecorder;// 计分员
public GameObject bow;// 弓弩(ArrowFactory中需要使用)
public GameObject target1, target2, target3, target4,target5,large_target; // 五种不同的靶子和一个巨型靶子
public GameObject arrow;// 当前射出的箭
private List<GameObject> arrows = new List<GameObject>(); // 待发射的箭
private List<GameObject> flyed = new List<GameObject>(); // 已经发射的箭
private Animator animator;//弓动画控制
private float force;//蓄力力量
private const float maxForce = 1f;//最大力量
private const float chargeRate = 0.1f;//每0.3秒蓄力的量
private float mouseDownTime;//记录鼠标蓄力时间
private bool isCharging=false;//是否正在蓄力
bool isFired=true; //是否已经将蓄力的箭发射
public Slider Powerslider;//蓄力条(在unity中编辑)
public SpotController currentSpotController;//当前射击点位控制器(通过ReadyController更改)
public bool readyToShoot = false;//是否可以开始射击(通过ReadyController更改)
public int shootNum = 0;// 剩余射击次数(通过ReadyController更改)
//加载箭和靶子
public void LoadResources()
{
bow = this.gameObject;
// 加载靶子
for(int i = 0; i < 3; ++i)
{
target1 = LoadTarget("normal_target", 10, new Vector3(-40, 12, 160-10*i)); // 静态普通靶子
target2 = LoadTarget("big_target", 5, new Vector3(-40, 7, 160-10*i)); // 静态大靶子
target3 = LoadTarget("small_target", 20, new Vector3(-40, 16, 160-10*i)); // 静态小靶子
}
target4 = LoadTarget("move_target", 15, new Vector3(0,0,0)); // 慢速移动靶子, 使用动画,自带位置
target5 = LoadTarget("quick_target", 20, new Vector3(0,0,0)); // 快速移动靶子,使用动画,自带位置
large_target = LoadTarget("large_target", 6, new Vector3(90, 30, 450)); // 静态大靶子
// 添加环数的分数
for(int i = 0; i < 4; ++i)
{
Transform child = large_target.transform.GetChild(i);
child.gameObject.AddComponent<TargetController>();
child.gameObject.GetComponent<TargetController>().score = 7 + i;
}
//加载箭(3个射击点共30支)
for(int i=0;i<18;i++){
GameObject arrow = arrowFactory.GetArrow();
arrows.Add(arrow);
}
}
// 加载靶子
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>().score = score;
return target;
}
//进行资源加载
void Start()
{
arrow = null;
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
gameObject.AddComponent<ArrowFactory>();
gameObject.AddComponent<ScoreRecorder>();
arrowFactory = Singleton<ArrowFactory>.Instance;
scoreRecorder = Singleton<ScoreRecorder>.Instance;
arrowManager = this.gameObject.AddComponent<CCActionManager>() as CCActionManager;
animator = GetComponent<Animator>();
animator.SetFloat("power", 1f);
director.currentSceneController.LoadResources();
}
void Update()
{
Shoot();
HitGround();
for(int i = 0; i < flyed.Count; ++i)
{
GameObject t_arrow = flyed[i];
if(t_arrow.tag == "ground"||t_arrow.transform.position.y < -10) //箭的位置低于-10
{
// 删除箭
flyed.RemoveAt(i);
arrowFactory.RecycleArrow(t_arrow);
}
}
}
void Shoot(){
//未到达射击点或该设计点箭矢已用完
if (!readyToShoot)
{
Powerslider.gameObject.SetActive(false);
return;
}
//按照鼠标按下的时间蓄力,每0.3秒蓄0.1的力(最多0.5)加到animator的power属性上,并用相应的力射箭
if (Input.GetMouseButtonDown(0) && isFired) // 0表示鼠标左键
{
transform.GetChild(4).gameObject.SetActive(true);// 设置动作中的箭可见
isFired = false;
mouseDownTime = Time.time; // 记录鼠标按下的时间
isCharging = true; // 开始蓄力
Powerslider.gameObject.SetActive(true);//显示蓄力条
animator.SetTrigger("start");
PlaySound1();//播放声音
}
//根据蓄力程度更新弓的动画
if (isCharging)
{
float holdTime = Time.time - mouseDownTime; // 计算鼠标按下的时间
force = Mathf.Min(holdTime / 0.3f * chargeRate, maxForce); // 计算蓄力的量,最大为0.5
Powerslider.value = force / maxForce; // 更新蓄力条的值
animator.SetFloat("power", force);
}
//鼠标左键弹起,此时进入hold动画
if(Input.GetMouseButtonUp(0))
{
animator.SetTrigger("hold");
isCharging = false;
}
//按下鼠标右键,将弓箭发射出去
if (Input.GetMouseButtonDown(1) && readyToShoot)
{
transform.GetChild(4).gameObject.SetActive(false);// 设置动作中的箭不可见
isFired = true;
animator.SetTrigger("shoot");
animator.SetFloat("power", force); // 将蓄力的量加到animator的power属性上
//发射箭矢
arrow = arrows[0];
arrow.SetActive(true);
flyed.Add(arrow);
arrows.RemoveAt(0);
arrowManager.ArrowShoot(arrow, transform.parent.forward,force);//方向使用主相机方向
//恢复
force =0;
Powerslider.value = 0;//清零蓄力条
animator.SetFloat("power", 0f);
PlaySound2();//播放声音
//更新剩余箭矢数量
shootNum--;
currentSpotController.shootNum--;
if (shootNum == 0)
{
readyToShoot = false;
}
}
}
// 检测箭是否射中地面
public void HitGround(){
RaycastHit hit;
if (arrow!=null && Physics.Raycast(arrow.transform.position, Vector3.down, out hit, 0.1f))
{
// 如果射线与地面相交
if (hit.collider.gameObject.name == "Terrain")
{
arrow.tag = "ground";
//将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
//使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
}
}
}
void PlaySound1()//拉弓
{
audioSource1.clip = pullSound; // 设置音频片段为拉弓音效
audioSource1.Play(); // 播放音效
}
void PlaySound2()//射箭
{
audioSource1.clip = shootSound; // 设置音频片段为拉弓音效
audioSource1.Play(); // 播放音效
}
//返回当前分数
public int GetScore(){
return scoreRecorder.score;
}
//返回当前射击点的分数
public int GetSpotScore(){
return currentSpotController.spotScore;
}
//返回当前射击点剩余的箭的数量
public int GetArrowNum(){
return shootNum;
}
}
3.View模块
(1)UserGUI(用户交互界面)
UserGUI挂在弓弩Crossbow对象上,负责在游戏运行期间绘制和展示玩家的相关信息,包括游戏状态、分数以及准星(瞄准辅助线)。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEngine.EventSystems.EventTrigger;
public class UserGUI : MonoBehaviour
{
private IUserAction action;
private GUIStyle playInfoStyle;
public bool atSpot;//是否在射击点(由ReadyController控制)
public int crosshairSize = 20;//准星线条长度
// Start is called before the first frame update
void Start()
{
action = this.transform.gameObject.GetComponent<IUserAction>();
playInfoStyle = new GUIStyle();
playInfoStyle.normal.textColor = Color.black;
playInfoStyle.fontSize= 25;
}
// Update is called once per frame
void Update()
{
}
private void OnGUI()
{
ShowPlayingPage();
}
public void SetIsAtSpot(bool isAtSpot)
{
atSpot = isAtSpot;
}
private void ShowPlayingPage()
{
//绘制游戏信息
GUI.Label(new Rect(10, 10, 60, 100), "正在游戏",playInfoStyle);
if (atSpot)
{
// Draw the message with the new GUIStyle
GUI.Label(new Rect(10, 50, 500, 100), "您已到达射击位,剩余射击次数:" + action.GetArrowNum().ToString(), playInfoStyle);
GUI.Label(new Rect(10, 90, 500, 100), "您在该靶点的射击分数:" + action.GetSpotScore().ToString(), playInfoStyle);
}
GUI.Label(new Rect(10, 130, 500, 100), "游戏总分数:" + action.GetScore().ToString(), playInfoStyle);
//绘制准星
float screenWidth = Screen.width;
float screenHeight = Screen.height;
float centerX = screenWidth / 2f;
float centerY = screenHeight / 2f;
// 设置准星颜色
GUI.color = Color.red;
// 绘制准星
GUI.DrawTexture(new Rect(centerX - crosshairSize / 2f + 15f, centerY + 5f , crosshairSize, 2f), Texture2D.whiteTexture);
GUI.DrawTexture(new Rect(centerX - 1f + 15f, centerY - crosshairSize / 2f + 5f, 2f, crosshairSize), Texture2D.whiteTexture);
// 恢复GUI颜色设置
GUI.color = Color.white;
}
}