简介

实验要求

image.png
本项目已完成所有基本要求,其中游戏与创新部分共完成3项:

  1. 效果类:实现了箭的轨迹并且实现了爆炸的粒子效果
  2. 场景与道具类:下载了多种场景资源,并基于此完成造景
  3. 玩法类:设计了简单运动靶编排系统,便于靶场编成,同时增加了靶的类型与箭的类型,使原本单纯的打靶玩法进阶为需要根据前靶组合,选择箭的种类并且合适时机射击的游戏。

    游戏演示

    游戏演示视频链接

    代码链接

    项目代码链接

    具体实现

    地形

    image.png
    使用地形terrain进行地形的绘制,同时导入草、树等资源,完成环境的设计。

    天空盒

    image.png
    导入天空盒资源后,利用脚本将资源导入天空盒列表中,并且随鼠标滚轮变换。

    void Update()
    {
        float scroll = Input.GetAxis("Mouse ScrollWheel");
        if(scroll != 0){
            if(scroll > 0){
                materialIndex--;
                materialIndex = (materials.Count + materialIndex) % materials.Count;
            } else {
                materialIndex++;
                materialIndex = (materials.Count + materialIndex) % materials.Count;                
            }
            RenderSettings.skybox = materials[materialIndex];
        }
    }

固定靶

固定靶分为Target与UpTarget,均使用三个圆柱形成的“环”构成,其中不同环对应的奖励不同,后续体现在游戏分数上。
image.png

public class Target : MonoBehaviour
{
    private bool IsHit;
    public string type;
    public float startTime;
    public string action;
    private Vector3 IniPosition;
    private Quaternion IniRotation;
    // Start is called before the first frame update
    private Animator animator;
    private RuntimeAnimatorController animatorController;
    void Start()
    {
        switch(action){
            case "LeftMove":
                animatorController = Resources.Load<RuntimeAnimatorController>("Animation/LeftMoveAnimator");
                break;
            case "RightMove":
                animatorController = Resources.Load<RuntimeAnimatorController>("Animation/RightMoveAnimator");
                break;
            case "Stand":
                animatorController = Resources.Load<RuntimeAnimatorController>("Animation/StandAnimator");
                break;
            case "CircleMove":
                animatorController = Resources.Load<RuntimeAnimatorController>("Animation/CircleMoveAnimator");
                break ;
            case "Still":
                animatorController = Resources.Load<RuntimeAnimatorController>("Animation/StillAnimator");
                break;
            default:
                break;
        }

        IniPosition = transform.position;
        IniRotation = transform.rotation;
        if(type == "motivate"){
            if (gameObject.GetComponent<Animator>() == null){
                animator = gameObject.AddComponent<Animator>();
                animator.runtimeAnimatorController = animatorController;
                animator.applyRootMotion = true;                
            }
            animator.speed = 0;
            Invoke("StartAnimation", startTime);
        }

    }

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

    public void Initialize(){
        CancelInvoke();
        gameObject.transform.position = IniPosition;
        gameObject.transform.rotation = IniRotation;
        animator.speed = 0f;
        if(type == "motivate"){
            IsHit = false;
            Invoke("Up", startTime);            
        }

    }

    // 被击倒时的动画控制
    public void HitDown()
    {
        if(type == "motivate" && !IsHit){
            animator.SetTrigger("Hit");
            IsHit = true;
        }

    }

    public void Up(){
        animator.speed = 1f;
        animator.SetTrigger("Up");
    }

    public void StartAnimation()
    {
        animator.speed = 1f;
    }
    
}

运动靶

其中运动靶与固定靶共用一个Target.cs脚本,不同的是,通过设置type为motivate,运动靶拥有了执行指定运动与运动开始时间,并且初始化及执行被击倒动画的功能。分别录制向左向右移动与上下倒伏的动画再利用动画机进行组合则可衍生出其他的动画组合。下图为向右移动的animator。
image.png

射击位

image.png
其中射击位作为进入指定Game的位点,其与GameManager关联,完成currentGame的切换。其中射击位的检测通过Collider完成,但需要打开isTrigger.

public class Spot : MonoBehaviour
{
    // private FirstPersonController firstPersonController;
    public Game game;
    private GameManager gameManager;
    private GameObject player;
    private bool hasTriggerd = false;
    private bool isOnSpot = false;
    Vector3 IniPosition;
    // Start is called before the first frame update
    void Start()
    {
        gameManager = GameManager.Instance;
    }

    // Update is called once per frame
    void Update()
    {
        if (!hasTriggerd) return;
        
        // 当进入范围时,可选择进入射击模式
        if(hasTriggerd && !isOnSpot){
            if(Input.GetKeyDown(KeyCode.F)){
                isOnSpot = true;
                gameManager.EnterGame(game);
            }
        } else if (isOnSpot){
            if(Input.GetKeyDown(KeyCode.F)){
                isOnSpot = false;
                gameManager.ExitGame();
            }            
        }
    }

    private void OnTriggerEnter(Collider other) {
        if(!hasTriggerd){
            if(other.gameObject.tag == "Player"){
                hasTriggerd = true;
            }
        }
    }

    private void OnTriggerExit(Collider other) {
        gameManager.ExitGame();
        isOnSpot = false;
        hasTriggerd = false;
    }

    // 将Player固定在Spot上
    private void MovePlayer(GameObject player){
        FirstPersonController firstPersonController= player.GetComponent<FirstPersonController>();
        player.transform.position = transform.position + new Vector3(0, 0.1f, 0);
        firstPersonController.enableJump = false;
        firstPersonController.playerCanMove = false;
        isOnSpot = true;
    }

    private void ReMovePlayer(GameObject player){
        FirstPersonController firstPersonController= player.GetComponent<FirstPersonController>();
        firstPersonController.enableJump = true;
        firstPersonController.playerCanMove = true;
        isOnSpot = false;
    }
}

public class GameManager : MonoBehaviour
{
    public List<Material> materials;
    private int materialIndex = 0;
    public Game currentGame;
    public static GameManager instance;
    public static GameManager Instance
    {
        get {
            if (instance==null)
            {
                instance = FindObjectOfType<GameManager>();
            }
            return instance;
        }
    }

    public void EnterGame(Game game){
        Debug.Log("game: " + (game != null));
        currentGame = game;
        Debug.Log("Cgame: " + (game != null));
        currentGame.uiController = FindObjectOfType<UIController>();
        currentGame.scoreController = FindObjectOfType<ScoreController>();
        currentGame.arrowFactory = FindObjectOfType<ArrowFactory>();
        currentGame.Initialize();
    }

    public void ExitGame(){
        currentGame = null;
    }
    
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        float scroll = Input.GetAxis("Mouse ScrollWheel");
        if(scroll != 0){
            if(scroll > 0){
                materialIndex--;
                materialIndex = (materials.Count + materialIndex) % materials.Count;
            } else {
                materialIndex++;
                materialIndex = (materials.Count + materialIndex) % materials.Count;                
            }
            RenderSettings.skybox = materials[materialIndex];
        }
    }
}

摄像机

在玩家对象的正上方设置一个摄像机,并将摄像机作为玩家对象的子对象(能够跟随玩家对象移动),然后创建纹理Texture,将摄像机的目标纹理设置。再创建画布Canvas,创建一个画布的子对象原始图像Raw Image,将原始对象的纹理设置为以上提到的纹理,就能够使摄像机的画面在游戏界面上显示。
image.png

声音

声音通过添加Component Audio Source实现,背景音乐则可在玩家对象的子组件上播放,而弓箭的射击与击中音效则需要利用脚本在合适时机播放。
image.png

游走

本游戏的游走通过unity资源商店中导入的FirstPersonController实现,其中场景中的靶子以及树木都有collider故玩家不能穿过。
微信图片_20241213020058_compressed.png

射击效果

射击效果分为普通箭效果与爆炸箭效果,普通箭射中靶后,触发对应环的射击UI显示,并且播放射中的音效,取消速度,在靶上停留1.5s后由ArrowFactory回收,爆炸箭的效果大致相同,但是加上了爆炸的粒子效果。

public class ArrowController : MonoBehaviour
{
    private float speed;
    private bool hasCollided = false;
    private GameManager gameManager;
    private AudioSource audioSource;
    public string Type;
    public TrailRenderer trailRenderer;
    // Start is called before the first frame update

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        gameManager = GameManager.Instance;
    }

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

    }

    public void Shoot(){
        // 获取变量
        Crossbow crossbow= FindObjectOfType<Crossbow>();
        speed = 50f;
        // 计算方向
        Quaternion bowRotation = crossbow.transform.rotation;
        Vector3 direction = bowRotation * Vector3.forward;

        // 设置速度
        Rigidbody rb = GetComponent<Rigidbody>();
        rb.velocity = direction * speed;
    }

    private void OnCollisionEnter(Collision other) {
        // 发生碰撞
        if (!hasCollided){
            // 取消速度
            Rigidbody rb = GetComponent<Rigidbody>();
            rb.isKinematic = true;
            // 回收bow
            StartCoroutine(RecycleArrow());

            hasCollided = true;
            if(other.gameObject.tag == "Ring" || other.gameObject.name == "animal"){
                //OnCollision?.Invoke(other);
                audioSource.Play();
                Vector3 hitPoint = other.contacts[0].point;
                if (Type == "Arrow"){
                    // NormalArrow
                    Vector3 up = other.gameObject.transform.up;
                    Target target = FindTarget(other.gameObject.transform);
                    gameManager.currentGame.uiController.ShowHitUI(other.gameObject.name, hitPoint, up, target.transform.parent.rotation);
                    int Score = gameManager.currentGame.scoreController.UpdateScore(other.gameObject); 
                    gameManager.currentGame.uiController.SetScore(Score);
                    target.HitDown();              
                } else {
                    // FireArrow
                    // 爆炸
                    transform.Find("SmallExplosionEffect").gameObject.SetActive(true);
                    Collider[] colliders = Physics.OverlapSphere(hitPoint, 3f);
                    HashSet<Target> targets = new HashSet<Target>();
                    foreach(Collider collider in colliders){
                        if(collider.gameObject.tag == "Ring" || collider.gameObject.name == "animal")
                            targets.Add(FindTarget(collider.gameObject.transform));
                    }
                    
                    foreach(Target target in targets){
                        Vector3 up = target.gameObject.transform.forward + transform.gameObject.transform.rotation.eulerAngles;
                        // UI
                        gameManager.currentGame.uiController.ShowHitUI("Boom", target.gameObject.transform.position, up, target.gameObject.transform.parent.rotation);

                        // Score
                        int Score = gameManager.currentGame.scoreController.UpdateScore(null);
                        gameManager.currentGame.uiController.SetScore(Score);

                        // Target
                        target.HitDown();
                    }
                                    
                }  
            } 
        }
    }

    private void OnCollisionExit(Collision other) {
        hasCollided = false;
    }

    private IEnumerator RecycleArrow() {
        yield return new WaitForSeconds(1f);
        gameManager.currentGame.arrowFactory.FreeArrow(gameObject);
    }

    private Target FindTarget(Transform ring){
        while(ring.gameObject.GetComponent<Target>() == null){
            ring = ring.parent;
        }
        return ring.gameObject.GetComponent<Target>();
    }

}

碰撞与计分

其中碰撞是在箭上检测的,进而调取所碰撞到的靶的对应的击倒方法。而计分则有每个游戏对象对应的ScoreController完成,根据击中的不同的对象,进行相应的加分与减分。

public class Game : MonoBehaviour
{
    private Target[] targets;
    [SerializeField]
    private int ArrowNum;
    [SerializeField]
    private int fireArrowNum;
    public UIController uiController;
    public ScoreController scoreController;
    public GameManager gameManager;
    public ArrowFactory arrowFactory;
    public string ArrowType = "Arrow";
    // Start is called before the first frame update
    void Start()
    {
        // gameManager = FindObjectOfType<GameManager>();
        targets = gameObject.GetComponentsInChildren<Target>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.G)){
            if (ArrowType == "Arrow"){
                ArrowType = "FireArrow";
            } else {
                ArrowType = "Arrow";
            }
            uiController.ShowWeapon(ArrowType);

        }
    }

    public void Initialize(){
        // 初始化靶子
        foreach (Target target in targets){
            if (target.type == "motivate")
                target.Initialize();
        }
        // 初始化分数
        scoreController.Initialize();

        // 初始化状态
        ArrowNum = 10;
        fireArrowNum = 10;
        uiController.SetArrowNum(ArrowNum);
        uiController.SetFireArrowNum(fireArrowNum);
        uiController.SetScore(0);
    }

    public void UpdateArrowNum(){
        if(ArrowType == "FireArrow"){
            fireArrowNum--;
            uiController.SetFireArrowNum(fireArrowNum);
        } else {
            ArrowNum--;
            uiController.SetArrowNum(ArrowNum);
        }
    }

    public bool IsArrowRunOut(){
        if(ArrowType == "FireArrow")
            return  fireArrowNum == 0;
        else 
            return ArrowNum == 0;
    }
}

public class ScoreController : MonoBehaviour
{
    public int Score{get; private set;}
    // Start is called before the first frame update
    void Start()
    {
        
    }

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

    public int UpdateScore(GameObject ring){
        if(ring == null) {
            Score += 30;
            print(Score);
            return Score;
        }
        switch(ring.name){
            case "Edge":
                Score += 20;
                break;
            case "Middle":
                Score += 50;
                break;
            case "Center":
                Score += 100;
                break;
            case "animal":
                Score -= 75;
                break;
        }
        return Score;
    }

    public void Initialize(){
        Score = 0;
    }
}

弓弩动画

image.png

public class Crossbow : MonoBehaviour
{
    public Animator anim{ get; private set; }
    public AudioSource audioSource;
    private bool animEnded = false;
    // private SceneController sceneController;
    private GameManager gameManager;
    // Start is called before the first frame update
    void Start()
    {
        anim = GetComponent<Animator>();
        audioSource = GetComponent<AudioSource>();
        gameManager = GameManager.Instance;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetMouseButton(0)){
            anim.SetBool("Fire",true);
        } else {
            anim.SetBool("Fire",false);
        }

        AnimatorStateInfo stateInfo = anim.GetCurrentAnimatorStateInfo(0);
        // 判断当前动画是否已播放完毕
        if (!animEnded && stateInfo.IsName("Shoot") && stateInfo.normalizedTime >= 1.0f)
        {
            animEnded = true; 
            // 射箭
            if(!gameManager.currentGame.IsArrowRunOut()){
                gameManager.currentGame.UpdateArrowNum();
                GameObject arrow = gameManager.currentGame.arrowFactory.GetArrow(transform.position, transform.rotation, gameManager.currentGame.ArrowType);
                audioSource.Play();
                arrow.GetComponent<ArrowController>().Shoot();                
            } 
        }

        if (stateInfo.normalizedTime < 1.0f && animEnded)
        {
            animEnded = false; // 重置为未完成,等待下一次发射
        }
    }
}


引用和评论

0 条评论