3D游戏设计 — 射箭 教程/代码 超详细!

本文详细描述了在Unity3D游戏中,如何通过C#编程实现弩的移动控制、镜头跟随、射击机制、分数计算以及解决了一些如树木静止问题的技术细节。
摘要由CSDN通过智能技术生成

顾名思义,整个射箭

游戏视频链接:3D游戏设计 — 射箭 大作业 修改版_哔哩哔哩bilibili

弩(玩家所操控的角色)的运动Crossbow_Move.cs

       (本代码挂载在弩上)

        在设计之初,这份c#的功能是接受键盘上WASD和方向键↑↓←→,将其转化为弩的运动。
弩会(朝着镜头方向)前进后退,但当镜头偏上或偏下(有y轴分量)时,弩不会朝着天上移动;

        在后续实验中发现:若先让弩运动,再计算镜头MainCamera的位置并赋值,则镜头会发生明显的颤抖。因此,直接让方向键同时控制弩和镜头的各自运动。

        但是这样也有个小问题:发生碰撞的时候弩应当停在原地(加入box collision组件即可),但此时镜头仍然会根据方向键的输入而移动。因此,最后同时保留了方向键控制镜头移动和位置的计算和赋值,这样才能既消除颤抖,又使镜头真正跟随弩的移动。
        (镜头位置的计算和赋值放在了CameraFollow.cs中)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
//功能:设计弩的移动(和静止)

public class Crossbow_Move: MonoBehaviour
{
    public float m_speed = 10f; //这个定义为公有变量,方便之后修改物体速度
    private float ypos;         //用来阻止弩的竖直方向移动上下
    private Vector3 offset;     //用来修改移动方向,使之在水平线上
    private Transform crossbow; //用来记录弩的transform对象变量
    private Transform camr;
    
    void Start()
    {
        //其实本代码套在弩上,crossbow就是this.transform
        crossbow = GameObject.FindGameObjectWithTag("Player").transform;
        crossbow.localEulerAngles = Vector3.zero;
        camr = GameObject.FindGameObjectWithTag("MainCamera").transform;
        Vector3 offset = new Vector3(0f, 1f, 0f);
        camr.position = crossbow.position + offset;
        ypos = crossbow.position.y;//初始y轴高度。日后都得是这个高度哦~
    }

    void Update()
    {
        //以下三行是用于消除镜头移动中莫名的颤抖
        GetComponent<Rigidbody>().velocity = Vector3.zero;
        GetComponent<Rigidbody>().constraints = RigidbodyConstraints.FreezeAll;
        GetComponent<Rigidbody>().constraints = RigidbodyConstraints.None;

        //以下是方向键/WASD移动
        offset = crossbow.forward;
        if (Input.GetKey(KeyCode.UpArrow) || Input.GetKey(KeyCode.W))//如果按下了↑
        {
            this.transform.Translate(new Vector3(0, -offset.y * m_speed * Time.deltaTime, m_speed * Time.deltaTime));
            camr.Translate(new Vector3(0, -offset.y * m_speed * Time.deltaTime, m_speed * Time.deltaTime));
        }
            
        if (Input.GetKey(KeyCode.DownArrow) || Input.GetKey(KeyCode.S) )//如果按下了↓
        {
            this.transform.Translate(new Vector3(0, offset.y * m_speed * Time.deltaTime, -1 * m_speed * Time.deltaTime));
            camr.Translate(new Vector3(0, offset.y * m_speed * Time.deltaTime, -1 * m_speed * Time.deltaTime));
        }
            
        if (Input.GetKey(KeyCode.RightArrow) || Input.GetKey(KeyCode.D) )//如果按下了→
        {
            this.transform.Translate(new Vector3(m_speed * Time.deltaTime, 0, 0));
            camr.Translate(new Vector3(m_speed * Time.deltaTime, 0, 0));
        }
            
        if (Input.GetKey(KeyCode.LeftArrow) || Input.GetKey(KeyCode.A))//如果按下了←
        {
            this.transform.Translate(new Vector3(-1 * m_speed * Time.deltaTime, 0, 0));
            camr.Translate(new Vector3(-1 * m_speed * Time.deltaTime, 0, 0));
        }
            

        //这三行也控制弩只在同一水平线上
        Vector3 true_pos = this.transform.position;
        //true_pos.z = (true_pos.z < 0) ? true_pos.z : 0;//这一行限制弩的活动区域
        true_pos.x = (float)Math.Round(true_pos.x,5);
        true_pos.y = (float)Math.Round(ypos, 0);
        true_pos.z = (float)Math.Round(true_pos.z,5);
        //this.transform.position = Vector3.Lerp(this.transform.position, true_pos, m_speed * Time.deltaTime);
        this.transform.position = true_pos;
    }

    void OnCollisionEnter(Collision col)
    {
        m_speed = 3f;//发生碰撞时暂时降低速度,防止穿模
    }
        
    void OnCollisionExit(Collision col)
    {
        m_speed = 5f;//结束碰撞时恢复速度
    }
       
}


镜头位置/朝向移动代码:CameraFollow.cs

        (本代码挂载在MainCamera上)

        主要功能:使镜头位置跟随弩的运动而运动;使镜头和弩的朝向都随着鼠标移动而转向。

        在这份代码中,RotateSpeed参数控制了镜头转向的灵敏度;
                加入了限制,使得镜头上移和下移都不能超过40°。
                镜头位置是弩(玩家)位置抬高1个单位(0,1,0) ,创造类似于第一人称视角的效果。

using UnityEngine;
using System.Collections;
using System;

//功能:
//1.镜头跟随着弩的移动而移动;
//2.镜头与弩的朝向随着鼠标移动而变化

public class CameraFollow: MonoBehaviour
{
    private Vector3 offset = new Vector3(0f, 1f, 0f);   //相机相对于弩的偏移量
    private Transform crossbow;                         //用来记录弩的对象变量
    private Vector3 pos;                                //
    public int RotateSpeed = 3;                         //镜头旋转速度,类似于游戏内置的灵敏度

    void Start()
    {
        crossbow = GameObject.FindGameObjectWithTag("Player").transform;    //寻找弩
        //crossbow.localEulerAngles = Vector3.zero;
    }

    void Update()
    {
        //通过鼠标位置来获得下一瞬间的弩朝向/镜头朝向angle
        Vector3 angle = Vector3.zero;
        angle.x = transform.localEulerAngles.x - Input.GetAxis("Mouse Y") * RotateSpeed;
        if ((angle.x > 40) && (angle.x < 180)) { angle.x = 40; }//对于弩的上下朝向设置限制
        if ((angle.x < 320) && (angle.x > 180)) { angle.x = 320 ;}
        angle.y = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * RotateSpeed;

        //弩和镜头的朝向都设置为angle
        crossbow.localEulerAngles = angle;
        this.transform.localEulerAngles = angle;

        //在移动时调整相机相对于弩之间的位置。如果只靠这个移动camera会导致卡顿,需要搭配crowmove中的相机移动方式
        Vector3 true_pos = crossbow.position;
        true_pos.x = (float)Math.Round(true_pos.x, 5);
        true_pos.y = (float)Math.Round(true_pos.y, 0);
        true_pos.z = (float)Math.Round(true_pos.z, 5);
        pos = true_pos + offset;
        this.transform.position = pos;
    }
} 

射击代码:Click_to_shoot.cs

        (本代码挂载在弩上)

        主要功能:使鼠标左键控制弩发射箭。
        射箭这一动作存在着CD,冷却时间中不允许发射下一根箭;
        箭的飞行速度取决于弩的蓄力时间长度,蓄力时间越长,动作机animator中的参数shoot_force值越大,最大不超过1f。
        此外,设置了允许射击的范围:靶子位置.z>0,而当弩(玩家)位置.z<0时才允许设计。

        接下来依次放出 动作机结构 / 模型包 / 代码
        动作机的大致结构:

        1.射箭cd转好时,点击鼠标左键,则先使holding state参数变为true,再开始蓄力。蓄力是不断修改shoot_force的值,最大为1。
        2.蓄力达到最大值时,动画会进入Hold状态,在松开鼠标时进入Shoot状态;若还未蓄力完成就松开,则动画会直接从Fill进入Shoot状态。
        3.松开鼠标、进入Shoot时,利用预制体Prefab中的箭矢模型,先设置箭的位置、速度、朝向,再实例化一支箭,

        弩的模型/动画/状态机都可以用下图的包中导入:

       代码:

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

//功能:点击/长按/发射等动作的控制

public class click_to_shoot : MonoBehaviour
{
    private Animator ani;                               //记录动作机,用于调整其中的参数
    private float force;                                //力度。会同步给animator中的shoot_force
    private GameObject arrow;                           //记录预制体,用于实例化
    private GameObject crossbow;                        //记录弩
    private Vector3 offset = new Vector3(0, 0.5f, 0);   //记录偏移量,用于箭的位置的生成
    private bool cd = false;                            //记录发射的冷却
    private bool legal = true;                          //记录弩是否在选定区域内
    private int quantity = 5;
    GUIStyle style;

    void Start()
    {   //各种初始化
        ani = GetComponent<Animator>();
        force = 0f;
        arrow = (GameObject)Resources.Load("Prefab/Arrow");
        crossbow = GameObject.FindGameObjectWithTag("Player");
        style = new GUIStyle();
        style.fontSize = 45;
        style.normal.textColor = Color.black;
    }

    void FixedUpdate()
    {
        //接下来的if_else作用为:判定玩家是否处于可设计射击位置,只有是的时候才能执行动作/生成箭矢
        if ( (this.transform.position.z < 0) && ( (this.transform.position.x + 10) % 30 <= 20 ))
        {
            if(legal == false) 
            {
                ani.SetFloat("shoot_force", 0f);
                force = 0f;
                quantity = 5;
            }
            legal = true;
            
        }
        else
            legal = false;


        if ( Input.GetMouseButton(0) && !cd && legal && (quantity > 0))//没在cd的时候,按下鼠标左键
        {
            ani.SetBool("Holding_state",true);//正在蓄力
            if (force < 1f)//蓄力的上限是1,范围是 range( 0, 1, 0.05 )
            {
                force += 0.05f;
                ani.SetFloat("shoot_force", force);
            }
        }
        else if (!Input.GetMouseButton(0) && !cd && legal)//没在cd,松开鼠标
        {
            //生成一支箭
            if ( (force > 0.04f) && (quantity > 0) )//区分没点和刚松手
            {
                cd = true;                  //进入cd
                quantity -= 1;
                //指定位置和方向,生成箭
                arrow.transform.position = offset + crossbow.transform.position;
                arrow.transform.rotation = crossbow.transform.rotation;
                Invoke("inite", 0.1f);      //0.1秒后生成一支箭(为了贴合弩的动画)
                Invoke("unenable", 0.9f);   //等待0.5s后,冷却完毕,允许下一次生成箭
            }
            ani.SetBool("Holding_state", false);
            ani.SetTrigger("Crossrow_Trigger");
        }
    }

    public void OnGUI()
    {
        string text = "Arrows : " + quantity.ToString();
        GUI.Label(new Rect(10, 100, Screen.width, 50), text, style);
    }

    void inite()
    {
        Instantiate(arrow);                 //生成箭
    }

    void unenable()
    {
        ani.SetFloat("shoot_force", 0f);    //生成之后再将力归零
        force = 0f;
        cd = false;                         //cd结束
    }
}

箭靶移动代码:targetMove.cs

        (本代码挂载在箭靶上)

        主要功能:控制靶子往返运动
        当达到左边界时转向右运动,右边界同理。左右边界都是事先计算好的。

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

//功能:让靶子做往返匀速运动

public class targetMove : MonoBehaviour
{
    private Vector3 offset = new Vector3(10,0,0);//偏移量
    private Vector3 left_boundary;
    private Vector3 right_boundary;
    private int dir;
    private Vector3 leftmove = new Vector3(-5, 0, 0); 
    private Vector3 rightmove = new Vector3(5, 0, 0);


    void Start()
    {
        dir = 1;//代表初始运动方向向左
        left_boundary  = this.transform.position - offset;
        right_boundary = this.transform.position + offset;
    }


    void Update()
    {

        if (dir>0)//向左运动
        {
            GetComponent<Rigidbody>().velocity = leftmove;
            if (this.transform.position.x <= left_boundary.x) 
                dir = 1 - dir;
        }
        else
        {
            GetComponent<Rigidbody>().velocity = rightmove;
            if (this.transform.position.x >= right_boundary.x)
                dir = 1 - dir ;
        }
    }
}

箭矢飞行/插在箭靶上:arrowSpeed.cs

        (本代码需要挂载在预制体箭矢模型上)

        主要功能:
                1.在箭矢没有击中任何东西时,让子弹箭矢飞;
                2.在箭矢击中靶子(特定物体)时,使箭矢静止;
                3.在箭矢击中其他东西时,使箭矢垂直落下。

此外,在OnGUI中设置了分数的显示。分数是通过勾股定理计算距离得到的。

我的靶子本质上是个厚度为0.2的Cube。每当发生碰撞时,都先获取碰撞点P,然后与碰撞到的靶子的中心点计算距离(斜边),最后利用斜边和厚度计算出碰撞点到靶子中心的距离。这个距离对应着击中靶子的环数,能够依此知道得分。

下面放出代码和靶子图案(直接拖动到Cube上即可)
score是个自制的命名空间,在ScoreManager.cs中实现。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using score;
//控制箭矢在运动全过程中的状态

public class arrowSpeed : MonoBehaviour
{
    private int stopflag = 0;
    public int speed = 10;
    private Animator ani;
    private Transform crossbow;
    private float force;
    Vector3 closestPoint;
    Vector3 dir;
    Vector3 down;
    GameObject my_target;
    ScoreManager scrmng;
    GUIStyle style;

    void Start()
    {
        crossbow = GameObject.FindGameObjectWithTag("Player").transform;
        force = crossbow.GetComponent<Animator>().GetFloat("shoot_force") * 20f;
        //Debug.Log("force:" + force.ToString());
        style = new GUIStyle();
        style.fontSize = 45;
        style.normal.textColor = Color.black;

        dir = crossbow.forward;
        dir.x = speed * dir.x;
        dir.y = speed * dir.y;
        dir.z = speed * dir.z;
        down.y = -5;
        scrmng = new ScoreManager();
    }
    public void OnGUI()
    {
        string text = "Total Scores : " + scrmng.getTotalScore().ToString();
        string text2 = "Scores : " + scrmng.getThisScore().ToString();
        GUI.Label(new Rect(10, 10, Screen.width, 50), text, style);
        GUI.Label(new Rect(10, 55, Screen.width, 50), text2, style);
    }
    void Update()
    {
        
        if (0==stopflag)//没发生碰撞
        {
            GetComponent<Rigidbody>().velocity = dir * force;
            GetComponent<Rigidbody>().useGravity = true;
        }
        else if (2 <= stopflag)//没打中靶子,受重力作用,且下坠
        {
            GetComponent<Rigidbody>().velocity = down;
            GetComponent<Rigidbody>().useGravity = true;
        }
        else if (1 == stopflag)//打中靶子,与其保持相对静止,记录分数
        {
            GetComponent<Rigidbody>().velocity = my_target.GetComponent<Rigidbody>().velocity;
            GetComponent<Rigidbody>().useGravity = false;
            
        }
        //else //没打中靶子,跟随物体移动或静止
        //{
        //    GetComponent<Rigidbody>().velocity = my_target.GetComponent<Rigidbody>().velocity;
        //}
    }

    void OnCollisionEnter(Collision col)
    {
        //Vector3 location = this.transform.position;
        //closestPoint = collision.collider.ClosestPoint(location);

        my_target = col.gameObject;//my_target是指撞到的物体。一般是靶子target,也有可能是箭/地面
        if ( (my_target.tag == "Target" ))
        {

            stopflag += 1;
            closestPoint = col.collider.ClosestPoint(this.transform.position);
            float distance = Vector3.Distance(closestPoint, my_target.transform.position);
            scrmng.setDistance(distance);
        }
        else
        {
            stopflag += 2;
        }
       
    }
}

靶子图案来源于百度图片。

得分管理代码:ScoreManager.cs

        (本代码不需要挂载)

        此处创建了命名空间score,这个命名空间被arrowSpeed中的OnGUI()调用,从而计算和显示分数。

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

namespace score{
    
    public class ScoreManager{
        public float distance = 0f;
        public static int this_score = 0;
        public static int total_score = 0;
        

        public void Start ()
        {
            
        }
        public void setDistance(float dd) 
        { 
            distance = (float)Math.Sqrt( dd * dd - 0.01f);
            //Debug.Log("distance : " + distance.ToString()); 
            this_score = 10 - (int)(distance / 0.2f);
            this_score = ( (this_score > 0) ? this_score : 0);
            //Debug.Log("this_score : " + this_score.ToString());
            total_score += this_score;
            //Debug.Log("score : " + total_score.ToString());
        }
        public int getThisScore()
        { return this_score; }
        public int getTotalScore() 
        { return total_score; }
    }

}

应对bug的代码:TreeStop.cs

        十分惭愧,在下的项目中,树木模型不能静止在原地,有时候会向上飘,有时候会沉入地下。为此,特地编写了一个简易的代码,让树(以及别的小东西)静止在原地。

        原理:速度不断设置为0;记录物体的初始位置和朝向,不断赋值给物体本身

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

public class TreeStop : MonoBehaviour
{
    private Vector3 pos;
    private Vector3 fwd;
    // Start is called before the first frame update
    void Start()
    {
        pos = this.transform.position;
        fwd = this.transform.forward;
        
    }

    // Update is called once per frame
    void Update()
    {
        GetComponent<Rigidbody>().velocity = Vector3.zero;
        this.transform.position = pos;
        this.transform.forward = fwd;
    }
}

天空盒切换代码:SkyBoxController.cs

        (本代码需要挂载在MainCamera上)

仅切换 “镜头内看到的” 天空盒skybox。在使用本代码时,需要在挂载后手动加入天空盒资源。

加入方式如下:选中挂载了代码的MainCamera,在Inspector内找到Materials队列,点+加入资源。天空盒资源在assret store中一搜一大把,我用的是Fantasy Skybox FREE。

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

//功能:每15秒切换一次天空盒

public class SkyBoxController : MonoBehaviour
{
    public Material[] materials = null;
    int index;

    void Start()
    {
        InvokeRepeating("ChangeSkyBox", 0, 15f);
    } 

    void ChangeSkyBox()
    {
        RenderSettings.skybox = materials[index];
        index++;
        index %= materials.Length;
    }
}

总结

        项目gitee仓库:出了点小问题没法上传,慢慢修。

        我在本次开发过程中,既写出了属于自己的游戏,也对过往学习的知识整了一次查缺补漏和运用,受益匪浅。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值