[3D游戏编程]Counter Strike:Primitive Campaign——打靶游戏

一、游戏要求

  • 基础分(2分):有博客;
  • 1-3分钟视频(2分):视频呈现游戏主要游玩过程;
  • 地形(2分):使用地形组件,上面有草、树;
  • 天空盒(2分):使用天空盒,天空可随玩家位置 或 时间变化 或 按特定按键切换天空盒;
  • 固定靶(2分):有一个以上固定的靶标;
  • 运动靶(2分):有一个以上运动靶标,运动轨迹,速度使用动画控制;
  • 射击位(2分):地图上应标记若干射击位,仅在射击位附近可以拉弓射击,每个位置有 n 次机会;
  • 驽弓动画(2分):支持蓄力半拉弓,然后 hold,择机 shoot;
  • 游走(2分):玩家的驽弓可在地图上游走,不能碰上树和靶标等障碍;
  • 碰撞与计分(2分):在射击位,射中靶标的相应分数,规则自定。

二、游戏实现

1、导入必要的包

  • Fantasy Skybox FREE   用于天空和地形
  • 十字弩 Classical Crossbow   弩和弓箭
  • Realistic Tree 9 [Rainbow Tree]   树
  • Crosshairs | 25+ Free Crosshairs Pack 准星

2、创建预制件

箭 Arrow

使用导入包中的箭,子对象MoveTrack把Trail Renderer中的Autodestruct取消勾选,HitPos添加以下两个component

弩 Crossbow

使用导入包的Crossbow,修改动画使得符合任务要求

在Empty到Pull需要start被触发;Pull中的Parameter选为Power,但这里将Power固定为0.3,因为要实现hold的效果;Hold中的Parameter选为holdPower,holdPower可以在0.3到1之间变化,本游戏通过滚轮调整;Hold到Shoot需要shoot触发

每个状态设置如下(Pull和Hold是Blend Tree)

target

自行制作,效果如下

        

三个环其实是三个很薄的圆柱,分别对应1、2、3分,Cube是靶身,Cube1是下面的杆子

每个环要加上Mesh Collider,但不要勾选Convex和Is Trigger,颜色自己制作并分别挂载在每个环上;Cube添加Box Collider。此外,以上物体要挂载RingController脚本,并将Ring Score按分值设置(Cube分值为0)

slide target

制作方法基本和target一样,只是多一个动画,实现滑动的功能

x坐标在第0帧为-30,第30帧为-35,第60帧为-30,y一直为0,z一直为20。

stage target和rotate target

和target基本相同,但由于这两个prefab都是用于闯关模式,不用计分,只检测命中,所以所有子对象不挂载RingController,而且子对象不设置Collider。在主对象中设置如下

stage target和rotate target只有动画不一样

stage target只有被击中后倒下的动画,Nothing用于过渡,什么动作都不执行,设置这个状态是因为Entry到下一个状态不能设置条件,Nothing到fall down需要isShot触发

动画实现也很简单,第0帧x为0,第60帧x为90,其他都不变

rotate target只是比stage target多一个旋转的动作

rotate动画实现如下

x最低点为-1.5,最高点为1.5,y最高点为3。这里要注意红线,在每个Key点使用Auto,才会让旋转看起来更圆滑,不会卡顿。

3、场景其他物体设置

Main Camera

添加以下两个component是因为,本游戏没有用“人”做碰撞体,所以为了模拟人会和物体发生碰撞,就在主相机上设置碰撞体。

各种树

使用导入包的树,但是要自己添加Mesh Collider。

Terrain

自由DIY

Second Camera

用于箭射出去后跟踪箭的走向,只有下面两个属性需要调整(在Component--Camera调)

GameObject

空对象,用于挂载脚本

Shoot point 和 Stage start

两个红色薄圆柱,一定要记得把Capsule Collider删掉!!!(在这里debug了好久)

Canvas

无关紧要的东西,只是顺便学一下怎么在场景里能有字,效果就是这个字在场景中是立体的,怎么创建可以查看其他资料,在此不展开讲述。

三、代码实现

类图

代码

Adapter模式

动作基类SSAction.cs

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 ISSActionCallback 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();
    }
}

动作管理类基类SSActionManager.cs

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
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()
    {
        foreach (SSAction ac in waitingAdd) actions[ac.GetInstanceID()] = ac;
        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();
    }
    protected void FixedUpdate()
    {
        foreach (SSAction ac in waitingAdd) actions[ac.GetInstanceID()] = ac;
        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();
    }
}

箭的射击动作ArrowShootAction.cs

using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public class ArrowShoot : SSAction
{
    public float arrowSpeed = 40f;  // 箭的速度
    private float arrowForce; //拉弓力度

    public static ArrowShoot GetSSAction(float force)
    {
        ArrowShoot shootarrow = CreateInstance<ArrowShoot>();
        shootarrow.arrowForce = force;
        return shootarrow;
    }
    // Start is called before the first frame update
    public override void Start()
    {
        // 摆脱弓的控制
        gameObject.transform.parent = null;
        // 设成物理模式
        gameObject.GetComponent<Rigidbody>().isKinematic = false;
        // 将箭的速度设为向前
        gameObject.GetComponent<Rigidbody>().velocity = transform.forward * arrowSpeed * arrowForce;
        
    }

    // Update is called once per frame
    public override void Update()
    {
        gameObject.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
    }

    public override void FixedUpdate()
    {
        // 回调颤抖动作
        if (gameObject.tag == "onTarget")
        {
            this.destroy = true;
            this.callback.SSActionEvent(this, this.gameObject);
        }
    }

}

箭的射击动作管理类ArrowShootAction.cs

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

public class ArrowShootManager : SSActionManager, ISSActionCallback
{
    // 箭飞行的动作
    private ArrowShoot arrowShoot;
    public MainController mainController;

    void Start()
    {
        // 设置MainController的arrow_manager
        mainController = (MainController)SSDirector.getInstance().currentController;
        mainController.arrow_manager = this;

    }

    //箭飞行
    public void Shoot(GameObject arrow, float force)
    {
        // 实例化一个飞行动作。
        arrowShoot = ArrowShoot.GetSSAction(force);
        // 调用SSActionmanager的方法运行动作。
        this.RunAction(arrow, arrowShoot, this);
    }
    public void SSActionEvent(SSAction source, GameObject arrow = null)
    {
        ArrowTremble tremble = ArrowTremble.GetSSAction();
        RunAction(arrow, tremble, this);
    }
}

箭的颤抖动作ArrowTremble.cs

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

public class ArrowTremble : SSAction
{
    float tremble_radius = 1.5f;// 颤抖的程度
    float tremble_time = 1.0f; // 颤抖的时间

    Vector3 arrow_pos;   // 箭的原位置

    public static ArrowTremble GetSSAction()
    {
        ArrowTremble tremble_action = CreateInstance<ArrowTremble>();
        return tremble_action;

    }

    // Start is called before the first frame update
    public override void Start()
    {
        // 得到箭中靶时的位置
        arrow_pos = this.transform.position;
    }
    // 实现箭的颤抖动作
    // Update is called once per frame
    public override void Update()
    {   // 更新时间,得到剩余的颤抖时间
        tremble_time -= Time.deltaTime;

        if (tremble_time > 0)
        {
            // 获取头部的位置
            Vector3 head_pos = this.transform.Find("HitPos").position;
            // 围绕箭头颤抖
            this.transform.Rotate(head_pos, tremble_radius);
        }
        else
        {
            // 将箭返回一开始的位置
            transform.position = arrow_pos;
            // 开始销毁动作
            this.destroy = true;
            // 回调为空
            this.callback.SSActionEvent(this);
        }
    }

    public override void FixedUpdate()
    {

    }
}

导演类SSDirector.cs

public class SSDirector : System.Object
{
    // singlton instance
    private static SSDirector _instance;

    public MainController currentController { get; set; }


    // get instance anytime anywhare!
    public static SSDirector getInstance()
    {
        if (_instance == null)
        {
            _instance = new SSDirector();
        }
        return _instance;
    }

}

单实例Singleton.cs

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;
        }
    }
}

计分器ScoreRecorder.cs

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

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

    // Update is called once per frame
    public void RecordScore(int ringscore)
    {
        //增加新的值
        score += ringscore;
    }
}

ArrowFactory.cs

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

public class ArrowFactory : MonoBehaviour
{
    private List<GameObject> dirtyArrow; // 正在被使用的arrow
    private List<GameObject> freeArrow; // 没有被使用的arrow

    public GameObject arrow = null;

    void Start()
    {
        dirtyArrow = new List<GameObject>();
        freeArrow = new List<GameObject>();
    }
    public GameObject GetArrow()
    {
        int arrowCount = freeArrow.Count;
        if (arrowCount == 0)
        {
            arrow = GameObject.Instantiate(
                Resources.Load<GameObject>("Prefabs/Arrow"), Vector3.zero, Quaternion.identity);
        }
        else
        {
            arrow = freeArrow[freeArrow.Count - 1];
            freeArrow.RemoveAt(freeArrow.Count - 1);
            // 箭在靶子上需要把标签改回来
            if (arrow.tag == "onTarget")
            {
                arrow.tag = "arrow";
            }
            arrow.gameObject.SetActive(true);
        }
        // 先让箭在镜头前面不动
        arrow.GetComponent<Rigidbody>().isKinematic = true;
        // 箭跟随视角移动
        arrow.transform.parent = Camera.main.transform;
        arrow.transform.localPosition = new Vector3(0, 0, 1);
        Vector3 cameraDirection = Camera.main.transform.forward;
        arrow.transform.forward = cameraDirection.normalized;

        dirtyArrow.Add(arrow);
        return arrow;
    }


    public void FreeArrow(GameObject arrow)
    {
        foreach (GameObject a in dirtyArrow)
        {
            if (arrow.GetInstanceID() == a.GetInstanceID())
            {
                a.SetActive(false);
                a.GetComponent<Rigidbody>().isKinematic = true;
                dirtyArrow.Remove(a);
                freeArrow.Add(a);
                break;
            }
        }
    }
}

各种预制件的Controller

主相机脚本CameraFlow.cs

挂载在主相机

using UnityEngine;
using System.Collections;

public class CameraFlow : MonoBehaviour
{
    public GameObject bow;
    private int gameState;
    public float moveSpeed;
    public LayerMask groundLayer;
    // 水平视角移动的敏感度
    public float sensitivityHor;
    // 垂直视角移动的敏感度
    public float sensitivityVer;
    // 视角向上移动的角度范围,该值越小范围越大
    public float upVer = -85;
    // 视角向下移动的角度范围,该值越大范围越大
    public float downVer = 85;
    // 垂直旋转角度
    private float rotVer;
    // 水平旋转角度
    private float rotHor;
    public float smoothness;

    private void Start()
    {
        rotVer = 0;
        rotHor = 0;
        sensitivityHor = 50f;
        sensitivityVer = 30f;
        smoothness = 10f;
        transform.localPosition = new Vector3(0, 3, -10);
        moveSpeed = 10f;
    }
    void Update()
    {
        // 更新gameState
        gameState = SSDirector.getInstance().currentController.GetGameState();
    }
    void FixedUpdate()
    {
        MoveCamera();
        RotateCameraTowardsMouse();
    }
    // 实现第一人称视角的移动
    private void MoveCamera()
    {
        if (gameState == 0)
            return;
        float horizontalInput = Input.GetAxis("Horizontal");
        float verticalInput = Input.GetAxis("Vertical");
        Vector3 movement = new Vector3(horizontalInput, 0f, verticalInput) * moveSpeed * Time.deltaTime;
        // 保持与地面的距离不变
        transform.Translate(new Vector3(movement.x, 0f, movement.z));
        transform.position = new Vector3(transform.position.x, 3f, transform.position.z);
    }
    // 实现视角转动
    private void RotateCameraTowardsMouse()
    {
        if (gameState == 0)
            return;
        // 获取鼠标上下的移动位置
        float mouseVer = Input.GetAxis("Mouse Y");
        // 获取鼠标左右的移动位置
        float mouseHor = Input.GetAxis("Mouse X");
        // 鼠标往上移动,视角其实是往下移,所以要想达到视角也往上移的话,就要减去它
        rotVer -= mouseVer * sensitivityVer;
        rotHor += mouseHor * sensitivityHor;
        // 限定上下移动的视角范围,即垂直方向不能360度旋转
        rotVer = Mathf.Clamp(rotVer, upVer, downVer);

        Quaternion targetRotation = Quaternion.Euler(rotVer, rotHor, 0);
        transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, smoothness * Time.deltaTime);
    }
}
副相机脚本SecondCamera.cs

挂载在副相机

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


public class SecondCamera : MonoBehaviour
{
    public float show_time = 3f; // 相机展示时间
    public bool show = false;
    private GameObject curArrow; // 当前跟随的箭
    private Vector3 offset; // 偏移量
    void Start()
    {
        show = false;
        curArrow = null;
        offset = new Vector3(0, 0, -2);
    }
    void Update()
    {
        if (show == true)
        {
            show_time -= Time.deltaTime;
            // 将相机位置设在箭的正后方
            transform.position = curArrow.transform.position + curArrow.transform.forward + offset;
            // 展示时间超过3秒,停止展示
            if (show_time < 0)
            {
                curArrow = null;
                show = false;
                this.gameObject.SetActive(false);
            }
        }
    }
    // 展示副相机
    public void ShowCamera(GameObject arrow)
    {
        curArrow = arrow;
        show = true;
        show_time = 3f;
        this.gameObject.SetActive(true);
    }
}
弩脚本BowController.cs

挂载在弩

using System.Linq.Expressions;
using UnityEngine;
using UnityEngine.UIElements;

public class BowController : MonoBehaviour
{
    public Camera cam;

    void Start()
    {
        // 跟随主相机移动
        transform.SetParent(cam.transform);
        transform.localPosition = new Vector3(1, -0.5f, 1);
    }
}
箭脚本ArrowController.cs

挂载在arrow

using UnityEngine;

public class ArrowController : MonoBehaviour
{
    private Rigidbody rb;
    private bool hit = false;

    void Start()
    {
        rb = GetComponent<Rigidbody>();
    }
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            hit = true;
            // 将箭的速度设为0
            rb.velocity = Vector3.zero;
            // 碰撞到地面时将刚体设置为运动学模式,停止物理运动
            GetComponent<Rigidbody>().isKinematic = true;
        }
    }

    private void FixedUpdate()
    {
        if (transform.parent == null && !hit)
            transform.rotation = Quaternion.FromToRotation(
                Vector3.forward, rb.velocity.normalized);
    }
    private void Update()
    {
        if (transform.parent != null && this.CompareTag("arrow"))
            hit = false;
    }
}
环脚本RingController.cs

挂载在target和slide target的子对象:三个薄圆柱和Cube

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

public class RingController : MonoBehaviour
{
    public int RingScore = 0; // 当前环的分值
    public MainController scene;
    public ScoreRecorder sc_recorder;
    // Start is called before the first frame update
    void Start()
    {
        scene = SSDirector.getInstance().currentController;
        sc_recorder = scene.recorder;
    }

    // Update is called once per frame
    void Update()
    {

    }

    void OnCollisionEnter(Collision collision)
    {
        GameObject arrow = collision.gameObject;
        //有箭中靶
        if (arrow.CompareTag("arrow"))
        {
            // 将箭的速度设为0
            arrow.GetComponent<Rigidbody>().velocity = Vector3.zero;
            // 使用运动学运动控制
            arrow.GetComponent<Rigidbody>().isKinematic = true;
            // 计分
            sc_recorder.RecordScore(RingScore);
            //标记箭为中靶
            arrow.tag = "onTarget";
        }
    }
}

闯关模式靶子脚本StageTargetController.cs

挂载在rotate target和stage target的主对象

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

public class StageTargetController : MonoBehaviour
{
    public MainController scene;
    public ScoreRecorder sc_recorder;
    public bool isShot;
    // Start is called before the first frame update
    void Start()
    {
        scene = SSDirector.getInstance().currentController;
        sc_recorder = scene.recorder;
        isShot = false;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    void OnCollisionEnter(Collision collision)
    {
        GameObject arrow = collision.gameObject;
        //有箭中靶
        if (arrow.CompareTag("arrow") && !isShot)
        {
            // 将箭的速度设为0
            arrow.GetComponent<Rigidbody>().velocity = Vector3.zero;
            // 使用运动学运动控制
            arrow.GetComponent<Rigidbody>().isKinematic = true;
            // 计分
            sc_recorder.RecordScore(1);
            //标记箭为中靶
            arrow.tag = "onTarget";
            isShot = true;
            gameObject.GetComponent<Animator>().SetTrigger("isShot");
        }
    }
}

两个接口Interface

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

public interface ISceneController
{
    void LoadResources();
}

using UnityEngine;

public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
    // 回调函数
    void SSActionEvent(SSAction source, GameObject arrow = null);
}

玩家交互界面UserGUI.cs

需要挂载在GameObject

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

public class UserGUI : MonoBehaviour
{
    private MainController action;
    GUIStyle tip_style = new GUIStyle(); 
    GUIStyle title_style = new GUIStyle();
    GUIStyle button_style = new GUIStyle();
    GUIStyle state_style = new GUIStyle();
    GUIStyle time_style = new GUIStyle();
    public Texture texture;
    private float time = 0f;

    // Start is called before the first frame update
    void Start()
    {
        action = this.transform.gameObject.GetComponent<MainController>();
        tip_style.fontSize = 30;
        tip_style.normal.textColor = Color.blue;

        title_style.normal.textColor = Color.red;
        title_style.fontSize = 40;

        button_style.normal.textColor = Color.red;
        button_style.fontSize = 20;

        state_style.normal.textColor = Color.red;
        state_style.fontSize = 30;

        time_style.normal.textColor = Color.red;
        time_style.fontSize = 20;

    }

    // Update is called once per frame
    void Update()
    {
        if (action.GetGameState() == 1)
        {
            // 实现射击
            // 点击鼠标右键拉弓
            if (Input.GetButtonDown("Fire2"))
            {
                action.LoadArrow();
            }
            if (action.GetisLoad())
            {
                // 调整射箭力量
                float scrollInput = Input.GetAxis("Mouse ScrollWheel") / 2; //向上滑+1,反之-1
                action.SetForce(scrollInput);
                // 左键射击
                if (Input.GetButtonDown("Fire1"))
                {
                    action.ShootArrow();
                }   
            }
            // 实现闯关模式计时
            if (action.GetIsStageMode() == 1)
                time += Time.deltaTime;
            // 进入/重启闯关模式
            if (Input.GetKeyDown(KeyCode.R))
            {
                time = 0;
                action.Restart();
            }
        }
        // 切换天空
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            action.SwitchMaterial();
        }
    }
    //
    private void OnGUI()
    {
        if (action.GetGameState() == 1)
        {
            // 显示准星
            int width = texture.width / 20;
            int height = texture.height / 20;

            Rect rect = new Rect( (Screen.width - width) / 2, 
                (Screen.height - height) / 2, width, height);
            GUI.DrawTexture(rect, texture);

            //显示各种信息
            GUI.Label(new Rect(20, 10, 150, 50), "Score: ", button_style);
            GUI.Label(new Rect(100, 10, 100, 50), action.GetScore().ToString(), state_style);

            GUI.Label(new Rect(20, 50, 150, 50), "ArrowNum: ", button_style);
            GUI.Label(new Rect(120, 50, 100, 50), action.GetArrowNum().ToString(), button_style);
            if(action.GetIsStageMode() != 0)
                GUI.Label(new Rect(20, 80, 150, 50), "Time: " + time + " s", time_style);
            if(action.GetIsStageMode() == 2)
                GUI.Label(new Rect(Screen.width / 2 - 250, Screen.height / 2, 400, 100),
                "闯关模式结束,请按R重新开始", tip_style);
        }
        //游戏未开始,检测开始按键
        else
        {
            GUI.Label(new Rect(Screen.width / 2 - 80, Screen.height / 2 - 320, 100, 100), "  Counter Strike:\nPrimitive Campaign", title_style);
            //开始按键如果按下,游戏开始
            if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2 + 100, 100, 50), "游戏开始"))
            {
                action.BeginGame();
            }
            GUI.Label(new Rect(Screen.width / 2 - 350, Screen.height / 2, 400, 100), 
                "使用方向键控制弓箭移动,鼠标右键拉弓,左键射击\n按r进入闯关模式,按tab切换天空", tip_style);
        }
    }
}

场景控制器MainController.cs

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

public class MainController : MonoBehaviour, ISceneController
{
    // 箭的动作管理者
    public ArrowShootManager arrow_manager;
    // 箭的工厂
    public ArrowFactory factory;
    // 副相机
    public Camera second_camera;
    //主相机
    public Camera main_camera;
    // 记录分数
    public ScoreRecorder recorder;
    // 弓
    public GameObject bow;
    // 箭
    public GameObject arrow;
    // 箭是否load
    private bool isLoad;
    private int arrow_num = 0;    //射出的箭数
    // 箭的队列
    private Queue<GameObject> arrows = new Queue<GameObject>();
    // 0是游戏未开始,1是游戏进行
    private int gameState = 0; 
    // 天空
    public List<Material> skyboxMaterials;
    // 当前天空下标
    private int cnt;
    // 练习模式靶子
    private List<GameObject> prac_target;
    // 是否进入闯关模式
    private int isStageMode; // 0是未进入,1是正在进行,2是完成
    // 闯关模式靶子
    private List<GameObject> stage_target;
    public void LoadResources()
    {
        SSDirector director = SSDirector.getInstance();
        director.currentController = this;

        bow = Instantiate(Resources.Load("Prefabs/Crossbow", typeof(GameObject))) as GameObject;

        LoadPracticeModeTarget();

        main_camera = Camera.main;
        second_camera = GameObject.Find("Second Camera").GetComponent<Camera>();
        second_camera.gameObject.SetActive(false);

        factory = gameObject.AddComponent<ArrowFactory>();

        recorder = gameObject.AddComponent<ScoreRecorder>();

        arrow_manager = gameObject.AddComponent<ArrowShootManager>();

        RenderSettings.skybox = skyboxMaterials[cnt];
    }
    // 加载练习模式靶子
    public void LoadPracticeModeTarget()
    {
        prac_target = new List<GameObject>();
        GameObject target1 = Instantiate(Resources.Load(
            "Prefabs/target", typeof(GameObject)), 
            new Vector3(-25, 0, 40), Quaternion.identity) as GameObject;
        GameObject target2 = Instantiate(Resources.Load(
            "Prefabs/target", typeof(GameObject)),
            new Vector3(-45, 0, 10), Quaternion.identity) as GameObject;
        GameObject target3 = Instantiate(Resources.Load(
            "Prefabs/target", typeof(GameObject)),
            new Vector3(-35, 0, 3), Quaternion.identity) as GameObject;
        GameObject target4 = Instantiate(Resources.Load(
            "Prefabs/slide target", typeof(GameObject))) as GameObject;
        prac_target.Add(target1);
        prac_target.Add(target2);
        prac_target.Add(target3);
        prac_target.Add(target4);
    }
    // 加载闯关模式靶子
    private void LoadStageTarget()
    {
        // 释放之前的
        if (stage_target != null && stage_target.Count > 0)
        {
            foreach(GameObject obj in stage_target)
            {
                Destroy(obj); // 这里可以用TargetFactory优化,避免重复生成删除
            }
        }
        stage_target = new List<GameObject>();
        GameObject target1 = Instantiate(Resources.Load(
            "Prefabs/stage target", typeof(GameObject)),
            new Vector3(73, 0.3f, -6), Quaternion.Euler(0, 90, 0)) as GameObject;
        GameObject target2 = Instantiate(Resources.Load(
            "Prefabs/stage target", typeof(GameObject)),
            new Vector3(75, 0.3f, 18), Quaternion.identity) as GameObject;
        GameObject target3 = Instantiate(Resources.Load(
            "Prefabs/stage target", typeof(GameObject)),
            new Vector3(85, 0.3f, 70), Quaternion.identity) as GameObject;
        GameObject target4 = Instantiate(Resources.Load(
            "Prefabs/stage target", typeof(GameObject)),
            new Vector3(115, 0.3f, 30), Quaternion.Euler(0, 135, 0)) as GameObject;
        GameObject target5 = Instantiate(Resources.Load(
            "Prefabs/stage target", typeof(GameObject)),
            new Vector3(145, 0.3f, 80), Quaternion.identity) as GameObject;
        GameObject target6 = Instantiate(Resources.Load(
            "Prefabs/rotate target", typeof(GameObject)),
            new Vector3(210, 0, 80), Quaternion.Euler(0, 90, 0)) as GameObject;
        stage_target.Add(target1);
        stage_target.Add(target2);
        stage_target.Add(target3);
        stage_target.Add(target4);
        stage_target.Add(target5);
        stage_target.Add(target6);
    }
    // 判断是否能射箭
    private bool canShoot()
    {
        // 获取主相机的位置
        Vector3 cameraPosition = Camera.main.transform.position;
        // 圆心和半径
        Vector3 circleCenter = new Vector3(-34f, 0f, -10f);
        float radius = 18f;
        // 计算相机位置与圆心的距离
        float distance = Vector3.Distance(cameraPosition, circleCenter);
        // 判断是否在圆内
        if (distance <= radius || isStageMode == 1)
            return true;
        return false;
    }
    // Start is called before the first frame update
    void Start()
    {
        LoadResources();
        isLoad = false;
        bow.GetComponent<BowController>().cam = main_camera;
        cnt = 0;
        isStageMode = 0;
    }

    // Update is called once per frame
    void Update()
    {
        // 更新天空
        RenderSettings.skybox = skyboxMaterials[cnt];
        if (gameState == 1)
        {
            foreach (GameObject arrow in arrows)
            {
                // 如果超出画面,设成运动学让箭不动
                if (arrow.transform.position.z > 150)
                {
                    arrow.GetComponent<Rigidbody>().isKinematic = true;
                }
            }
            // 场景只能有3支箭
            if (arrows.Count > 3)
            {
                // 获取队列第一个
                GameObject tempArrow = arrows.Dequeue();
                factory.FreeArrow(tempArrow);
            }
            // 更新stageMode状态
            setStageMode();
        }
        
    }
    // 返回是否装箭
    public bool GetisLoad()
    {
        return isLoad;
    }
    // 装箭
    public void LoadArrow()
    {
        if (gameState == 1 && !isLoad && canShoot())
        {
            //从工厂得到箭
            arrow = factory.GetArrow();
            arrows.Enqueue(arrow);
            isLoad = true;
            // 隐藏arrow
            arrow.GetComponent<Renderer>().enabled = false;
            arrow.GetComponentInChildren<TrailRenderer>().enabled = false;
            bow.GetComponent<Animator>().SetTrigger("start");
        }
    }
    // 设定力量
    public void SetForce(float scrollInput)
    {
        float force = bow.GetComponent<Animator>().GetFloat("holdPower") + scrollInput;
        force = Mathf.Clamp(force, 0.3f, 1);
        bow.GetComponent<Animator>().SetFloat("holdPower", force);
    }
    // 射箭
    public void ShootArrow()
    {
        if (gameState == 1 && isLoad && canShoot())
        {
            // 通过管理类发射动作
            // 显现arrow
            arrow.GetComponent<Renderer>().enabled = true;
            arrow.GetComponentInChildren<TrailRenderer>().enabled = true;
            float force = bow.GetComponent<Animator>().GetFloat("holdPower");
            arrow_manager.Shoot(arrow, force);
            bow.GetComponent<Animator>().SetTrigger("shoot");
            bow.GetComponent<Animator>().SetFloat("holdPower", 0.3f);

            //开启副相机
            second_camera.GetComponent<SecondCamera>().ShowCamera(arrow);
            arrow_num += 1;
            isLoad = false;
        }
    }
    // 返回当前分数
    public int GetScore()
    {
        return recorder.score;
    }
    // 得到射出的箭的数量
    public int GetArrowNum()
    {
        return arrow_num;
    }
    // 开始闯关模式
    public void Restart()
    {
        arrow_num = 0;
        recorder.score = 0;
        main_camera.transform.position = new Vector3(50.69f, 0f, -10f);
        isStageMode = 1;
        LoadStageTarget();
        foreach (GameObject arrow in arrows)
        {
            factory.FreeArrow(arrow);
        }
        arrows.Clear();
    }
    // 开始游戏
    public void BeginGame()
    {
        gameState = 1;
        // 隐藏鼠标
        Cursor.visible = false;
    }
    // 得到游戏的状态
    public int GetGameState()
    {
        return gameState;
    }
    // 更换天空
    public void SwitchMaterial()
    {
        cnt = (cnt + 1) % skyboxMaterials.Count;
    }
    // 获取当前闯关模式状态
    public int GetIsStageMode()
    { return isStageMode; }
    // 用于更新闯关模式状态
    private void setStageMode()
    {
        if (isStageMode == 1)
        {
            if (stage_target.Count == 0)
                return;
            // 所有靶子都被击中,该轮闯关游戏结束
            foreach (GameObject tar in stage_target)
            {
                if (tar.GetComponent<StageTargetController>().isShot == false)
                {
                    Debug.Log(tar.GetComponent<StageTargetController>().isShot);
                    return;
                }
            }
            isStageMode = 2;
        }
    }
}

四、项目亮点

本次项目最大亮点在于,实现了箭的抛物线运动,成功模拟了箭在实际中的运动,该功能主要是靠以下代码实现:

transform.rotation = Quaternion.FromToRotation(
                Vector3.forward, rb.velocity.normalized);

这行代码使得箭在发射出去之后,顺着自己的运动方向发生旋转,从而模拟出箭的抛物线运动。如果不使用这行代码,而只使用箭本身的刚体属性,就会出现不是箭头着地,而是箭身着地,着地的箭头朝向取决于发射时的朝向,而不是向着地面或者靶子。

五、游戏展示

Counter Strike: Primitive Campaign

六、项目地址

Ivan53471/CrossBow (github.com)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值