unity小游戏——第一人称射箭游戏

一、前言

本文将介绍如何使用 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 限制垂直视角范围,避免角色过度仰望或俯视。
  • 传送功能:

    • 设置按键 Alpha1Alpha3 实现快速传送到不同的射击位置。
    • 使用 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)
  • 作用:回收箭对象,将其从场景中移除并销毁。
  • 实现
    1. 将箭设置为不可激活
    2. 使用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),并控制是否显示该蓄力条。
  • 复用和回收箭矢

    • 通过ArrowFactoryCCActionManager管理箭矢的创建和回收。
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;

    }

}


五、参考文献

Unity3D教程:弓箭射击游戏开发基础

Unity--第一人称射箭游戏_unity 射箭-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值