Unity游戏制作之射箭游戏

目录

前言

具体实现

1. 人物游走

2. 地形

3. 天空盒变换

​编辑

4. 靶子制作&&碰撞与计分

固定靶

运动靶

5. 射击位的实现

6. 弓弩变化

7. 其他部分

总结


前言

         3D游戏制作的大作业,先放要求:

        游戏成果截图:

下面将按功能讲解具体实现

具体实现

        游戏的整体框架沿用之前的架构,用工厂单实例来产生箭射出。整体结构如下:

        工欲善其事,必先利其器。首先要导入资源包,包括天空盒资源、树草资源、靶子资源和十字弩资源。这些都是免费资源~

1. 人物游走

        游戏需要让玩家在地图中以第一人称视角在地图上游走,包括前后左右移动、跳跃和移动视角。首先要创建玩家对象,这里创建的是一个胶囊体capsule,去除掉原来的胶囊碰撞体,添加Character Controller作为新的碰撞体。

        为了实现第一人称的效果,把相机调整到人物的上半部分,并挂载到人物上作为人物的子对象。射击用的十字弩也调整到合适位置并,挂载到相机下作为相机的子对象,这样摄像机移动时弓也能同步移动。为了检测玩家到地面的距离(防止一直跳上天的bug),还要添加一个检测对象在人物底部。这样对玩家对象的创建就结束了。

        (个人想法)胶囊体什么都没有感觉有点突兀,所以直接在mesh render中去掉材料了,按一个减号就行,这样就能让人物变得透明同时还有碰撞体积。

        用代码来控制人的移动、跳跃和相机视角的转动,下面两个代码分别挂载到人物和相机上。这两段代码是在网上搜的,但是现在找不到来源了……

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class PlayerController : MonoBehaviour
{
    //获得player的CharacterController组件
    private CharacterController cc;
 
    public float moveSpeed = 80.0f;//移动速度
 
    public float jumpSpeed = 50.0f;//跳跃速度
 
    //定义获得按键值的两个变量
    private float horizontalMove, verticalMove;
 
    //定义三维变量dir控制方向
    private Vector3 dir;
 
    //重力
    private float gravity = 9.8f;
 
    private Vector3 velocity;//用来控制Y轴速度
 
    //我们只需要检测player是否在地上就可以了,这里我们可以使用Physics中的CheckSphere方法,如果定义的球体和物体发生碰撞,返回真
    //为了使用这个方法,我们需要定义几个变量
    public Transform groundCheck;//检测点的中心位置
    public float checkRedius;//检测点的半径
    public LayerMask groundLayer;//需要检测的图层
    //布尔值来存储CheckSphere的返回值
    public bool isGround;
 
 
    private void Start()
    {
        //获取player的CharacterController组件
        cc = GetComponent<CharacterController>();
        //锁定鼠标后再解锁,鼠标将自动回到屏幕中心
        Cursor.lockState = CursorLockMode.Locked;
        // Cursor.lockState = CursorLockMode.None;
        //隐藏鼠标
        Cursor.visible = false;
    }
    private void Update()
    {
        isGround = Physics.CheckSphere(groundCheck.position,checkRedius,groundLayer);
        if(isGround && velocity.y < 0)
        {
            velocity.y = -2f;
        }
 
        horizontalMove = Input.GetAxis("Horizontal") *2 * moveSpeed;
        verticalMove = Input.GetAxis("Vertical") * 2 * moveSpeed;
 
        dir = transform.forward * verticalMove + transform.right * horizontalMove;
        cc.Move(dir * Time.deltaTime);
 
        
        //在一瞬间有一个向上的速度,在过程中也会随着重力慢慢下降,如果想要让它只跳跃一次的话,加上isGround就行了
        if(Input.GetKeyDown(KeyCode.Space) && isGround)
        {
            velocity.y = jumpSpeed*1.4f;
        }
 
        velocity.y -= gravity * Time.deltaTime;
        //再用CharacterController的Move方法来移动y轴
        cc.Move(velocity * Time.deltaTime);
    }
}

        控制相机跟随:

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

 
public class CameraController : MonoBehaviour
{
    //我们通过控制Player的旋转方法,来控制相机视角的左右移动,所以我们需要一个Player的Tranform
    public Transform player;
 
 
    //定义两个float变量,来获取鼠标移动的值
    private float mouseX, mouseY;
    //我们可以给鼠标增加一个灵敏度
    public float mouseSensitivity = 800f;
 
    //mouseY中的GetAxis方法会返回-1到1之间的浮点数,在鼠标移动的时候,数值会随着方向的变化而变化,在鼠标不动时,数值会回弹到0,所以我们就会遇到鼠标上下移动时回弹的问题
    public float xRotation;
 
    private void Update()
    {
        //在Update方法中,我们使用输入系统中的GetAxis方法来获取鼠标移动的值,乘以鼠标灵敏度再乘以Time.deltatime,鼠标移动的值就这样得到了
        //Input.GetAxis:它会在鼠标移动相应对应轴的过程中返回 -1 到 1 的值
        mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
        mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;
 
        xRotation -= mouseY;
 
        //使用数学函数Clamp限制
        xRotation = Mathf.Clamp(xRotation,-50f,60f);
 
        //Vector3.up是向上的一个三维变量,和一个0,1,0的三维变量是一样的
        //我们需要控制player的y轴旋转才能让它左右旋转
        player.Rotate(Vector3.up * mouseX);
        //接下来我们要选转相机了,我们使用tranform.localRotation方法,让相机上下旋转,使用localRotation就可以不被父对象旋转影响,造成一些奇怪的问题
        //因为localRotation是属性,我们还要给他赋值
        transform.localRotation = Quaternion.Euler(xRotation, 0, 0);
    }
}

2. 地形

        自行创建地形terrain,用自带的工具自行发挥创作。为了让上面的groundcheck能检测到东西,需要给地形的Layer设置成Ground,这个是需要自己创建并添加的。

        

        种的树和草用的都是导入资源包里的预制。初始的预制是没有碰撞体积的,要想有碰撞体积,我采取的方法是编辑预制,根据树的形状手动加碰撞体,然后用让人物变透明的一样的方法让拟合的物体变透明。

3. 天空盒变换

        天空盒资源用的是导入的资源包Fantasy Skybox FREE。作业要求是天空盒变化就好,但为了视觉效果更真实,让光照也跟着天空盒一起变化,光照的方向尽可能和贴图中太阳的方向一样。为了让场景的视觉效果更好,还调整了一系列参数,参考的教程是这个:

Unity中调整光照特效的7个技巧_unity怎么让场景变亮-CSDN博客

        下面是控制天空盒变化的代码,里面的一些数字是根据场景手动调整的。

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

public class ChangeSkyBox : MonoBehaviour
{
    public Material[] mats;
    private int index=0;
    // 获取Directional Light组件
    private Light directionalLight;
    public Gradient lightColorGradient;
    public AnimationCurve lightIntensityCurve;

    // Start is called before the first frame update
    void Start()
    {
        mats = Resources.LoadAll<Material>("Materials/skyboxs");
        RenderSettings.skybox = mats[0];
        index ++;
        directionalLight = GameObject.Find("Directional Light").GetComponent<Light>();
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.LeftShift)){
            ChangeBox();
        }
        ChangeLight();
    }
    public void ChangeBox()
    {
        RenderSettings.skybox = mats[index];
        index++;
        index %= mats.Length;
    }
    public void ChangeLight(){
        float currentTime = index*6f;
        // 计算当前的光强度
        float intensity = lightIntensityCurve.Evaluate(currentTime);
        // 计算当前的光颜色
        Color color = lightColorGradient.Evaluate(currentTime / 24f);
        // 设置光源的方向(模拟太阳的移动)
        float rotationAngle = (currentTime - 5f) * 15f; // 每小时15度
        directionalLight.transform.rotation = Quaternion.Euler(rotationAngle, -85f, 0f);

        // 设置光源的强度和颜色
        // directionalLight.intensity = intensity;
        directionalLight.color = color;
    }
}

        下面分别是早晨(6:00),中午(12:00),傍晚(18:00)和夜晚(24:00)的效果:

4. 靶子制作&&碰撞与计分

        靶子的资源用的是Military target(这个模型不知道为什么很小,放大50倍感觉才正常点)。要求射中不同区域得不同的分数,所以需要修改一下。这里把靶子分成3个有效击中区域,给每个区域制作一个用来检测碰撞的射击环(其实是个圆盘)。环的大小和厚度都是手动调整的,调成透明的方法也和前面一样。

        手动添加的碰撞体积用的都是mesh colider,其中除了background,都勾选上isTrigger选项,这样代码才能通过OnTriggerEnter函数检测到碰撞(勾选这个选项后实际上没有碰撞效果,箭会穿过去,但是后面有background兜着,所以不会穿过去)。

        计分就由挂载到环上的代码来实现,当发生碰撞后,就把箭停下,然后设成靶子的子对象,并且使用运动学控制,这样就能实现箭插在靶子上的效果。碰撞发生后,用专门的计分器ScoreController计分。

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

public class RingController : MonoBehaviour
{
    //当前环的分值
    public int RingScore ;
    public ISceneController scene;
    public ScoreRecorder sc_recorder;
    // Start is called before the first frame update
    void Start()
    {
        scene = Director.getInstance().currentSceneController as FirstController;
        sc_recorder = Singleton<ScoreRecorder>.Instance;
    }

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

    }
    //碰撞检测,如果箭击中该环,就响应。

    void OnTriggerEnter(Collider collision){
        Debug.Log("trigger");
        Transform arrow = collision.gameObject.transform;
        Debug.Log(arrow);
        if (arrow == null)
        {
            return;
        }
        if (arrow.tag == "arrow")
        {
            //将箭的速度设为0
            arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
            // Debug.Log("击中"+RingScore);
            //使用运动学运动控制
            arrow.GetComponent<Rigidbody>().isKinematic = true;
            arrow.transform.parent = this.transform.parent;         // 将箭和靶子绑定
            //计分
            sc_recorder.Record(RingScore);

        }
    }
}
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;

public class ScoreRecorder : MonoBehaviour
{
    int score;
    public FirstController firstController;
    public UserGUI userGUI;
    // Start is called before the first frame update
    void Start()
    {
        firstController = (FirstController)Director.getInstance().currentSceneController;
        firstController.scoreController = this;
        userGUI = this.gameObject.GetComponent<UserGUI>();
    }

    public void Record(int ringscore) {
        score += ringscore;
        userGUI.score = score;
    }
}

固定靶

        把靶子加载到固定位置即可。我在这里受平时玩的一个fps游戏启发,设计了一个可交互的固定靶,可以自行调整一个固定靶的位置。

        实现逻辑很简单,获取游戏对象,改变位置即可。为了方便(其实好像也不方便),这里在每个块上面都挂了代码,这里展示4m的:

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

public class Interact : MonoBehaviour
{
    GameObject target;  //需要移动的游戏对象
    Vector3 origin_position;
    Transform origin_transform;
    // Start is called before the first frame update
    void Start()
    {
        origin_position = new Vector3(75f,8f,55f);
        target = GameObject.Find("move-fixed");
        // Debug.Log(origin_transform.position);
    }

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

    }

    void OnTriggerEnter(Collider collision)
    {        
        // 获取被碰撞的物体
        GameObject collidedObject = collision.gameObject;
        if(collidedObject.tag == "arrow"){
            // Debug.Log(origin_transform.position);
            target.transform.position = origin_position + new Vector3(0,0,8f);
        }
    }
}

运动靶

        给游戏对象自行制作相应的动画animation,上课都说过,这里不再介绍。

5. 射击位的实现

       射击位也是通过碰撞实现。把射击区域设计成偏平的立方体,刚好能检测到碰撞的那种,这样看起来就像是地毯了。挂载代码到射击区域上,玩家进入区域时才通知firstcontroller可以射击。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class ShootArea : MonoBehaviour
{
    //是否可以射箭
    private bool canShoot;
    public FirstController firstController;

    void Start(){
        firstController = (FirstController)Director.getInstance().currentSceneController;
    }
 
    public void OnTriggerEnter(Collider collider)
    {
        if (collider.gameObject.tag == "Player")
        {
            canShoot = true;
            firstController.AreaCallBack(canShoot);
        }
    }
 
 
    private void OnTriggerExit(Collider collider)
    {
        if (collider.gameObject.tag == "Player")
        {
            canShoot = false;
            firstController.AreaCallBack(canShoot);
        }
    }
}

6. 弓弩变化

        根据导入资源的动作自行制作一个状态机,和上课讲的状态机一样,其中empty-pull和hold都是混合树,通过这两个混合树来实现蓄力拉弓的动作。

        射箭的逻辑由ShootControl控制,左键点击射箭,左键长按蓄力,长按时点击右键hold,hold后再次点击左键射出。通过一系列if else判断来检测鼠标状态,其中长按的检测用到了简单的协程;通过设置参数来控制动画,参数设置用setTrigger函数和setFloat实现。

        还要从firstController中获取是否能射箭和剩余箭数量的信息,只有在设计区域并且有箭时才可以射箭。

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

public class ShootControl : MonoBehaviour
{
    public Animator animator;
    public FirstController firstController;
    public float power = 0.4f;
    private bool isHolding = false;
    private bool isMouseDown;
    private bool isMouseLongPressed;

    private float longPressDuration = 0.2f; // 定义长按的持续时间

    void Start(){
        animator = GetComponent<Animator>();
        firstController = (FirstController)Director.getInstance().currentSceneController;
    }

    private void Update()
    {
        // 长按左键不断增加力量
        if (isMouseLongPressed && !isHolding)
        {
            power = Mathf.Min(power + Time.deltaTime, 1f);
        }
        // Debug.Log(power);
        animator.SetFloat("power", power);
        if(firstController.GetArea() && firstController.arrowNum>0){
            ClickCheck();
        }
        
    }

    public void ClickCheck(){
        // 按下左键
        if (Input.GetMouseButtonDown(0))
        {
            // Debug.Log("单击左键");
            if(!isHolding){
                isMouseDown = true;
                isMouseLongPressed = false;

                // 开始协程检测长按
                StartCoroutine(CheckLongPress());
                // 触发start
                animator.SetFloat("power", power);
                animator.SetTrigger("start");
            }
            else{
                ShootAnimator();
            }
        }
        else if(isMouseLongPressed && Input.GetMouseButtonDown(1)){//right key down
            // Debug.Log("右键按下");
            isHolding = true;
            // 停止协程
            StopCoroutine(CheckLongPress());
            isMouseLongPressed = false;

            animator.SetFloat("hold power", power);
            animator.SetTrigger("hold");
        }
        // 松开左键
        else if (isMouseDown && Input.GetMouseButtonUp(0))
        {
            isMouseDown = false;
            if(!isHolding){
                // Debug.Log("松开左键");
                isMouseLongPressed = false;

                // 停止协程
                StopCoroutine(CheckLongPress());
                //触发hold
                animator.SetFloat("hold power", power);
                // Debug.Log("hold power:"+power+".");
                animator.SetTrigger("hold");
                // 触发shoot
                ShootAnimator();
            }
        }
    }

    private IEnumerator CheckLongPress()
    {
        yield return new WaitForSeconds(longPressDuration);
        // 如果鼠标处于按下状态,则表示长按
        if (isMouseDown)
        {
            // Debug.Log("长按左键");
            isMouseLongPressed = true;
        }
        
    }

    private void ShootAnimator(){
        animator.SetTrigger("shoot");
        firstController.ShootCallback(true, power);
        isHolding = false;
        power = 0.4f;        
    }
}

7. 其他部分

        以上就是得分点的实现代码,但是只有这些是不完整的,还有其他比较重要的代码。比如射箭的动作控制:

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

public class CCActionManager : SSActionManager, IActionCallback 
{
    public FirstController sceneController;
    public FlyAction action;
    public ArrowFactory factory;

    public GameObject arrow;
    public GameObject crossbow;
    public float forceRate=0.33f;
    
    // Start is called before the first frame update
    protected new void Start()
    {
        sceneController = (FirstController)Director.getInstance().currentSceneController;
        sceneController.actionManager = this;
        factory = Singleton<ArrowFactory>.Instance;
        crossbow = GameObject.Find("Crossbow");
    }

    public void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Completed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null) {
            factory.FreeArrow(source.transform.gameObject);
    }

    public void ShootArrow(float power){
        Quaternion bowRotation = crossbow.transform.rotation;

		arrow = factory.GetArrow();
		Vector3 shootDirection = bowRotation * Vector3.up;
        // Debug.Log(shootDirection);
		arrow.GetComponent<Rigidbody>().AddForce(shootDirection * power * forceRate, ForceMode.Impulse);
        arrow.transform.parent = null;
        action = FlyAction.GetSSAction();
        this.RunAction(arrow, action, this);
    }
}

        还有一个就是前面经常提到的firstController,它是代码的主体控制器,要把它挂载到玩家对象上来实现一系列功能的。

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

public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
    public CCActionManager actionManager;
    public ArrowFactory factory;
    public UserGUI userGUI;
    public GameObject bow;
    public ScoreRecorder scoreController;
    float shotpower =0.4f;
    bool shot = false;
    public int arrowNum = 10;
    bool inArea = false;

    void Awake() {
        Director director = Director.getInstance();
        bow = GameObject.Find("Crossbow");

        director.currentSceneController = this;
        director.currentSceneController.LoadSource();

        gameObject.AddComponent<UserGUI>();
        gameObject.AddComponent<CCActionManager>();
        gameObject.AddComponent<ScoreRecorder>();
        gameObject.AddComponent<ArrowFactory>();
        
        factory = Singleton<ArrowFactory>.Instance;
        userGUI = gameObject.GetComponent<UserGUI>();
    }
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if(inArea && arrowNum>0 ){
            Shoot(shotpower);
        }
        userGUI.arrowNum = arrowNum;
    }

    public void LoadSource(){

    }

    public void Shoot(float shootpower){
        if(shot){
            actionManager.ShootArrow(shootpower);
            shot = false;
            arrowNum --;            
        }
    }
    public void gameOver(){

    }

    public void ShootCallback(bool isShot, float power)
    {
        shotpower = power;
        shot = isShot;
    }

    public void AreaCallBack(bool inArea){
        this.inArea = inArea;
        if(inArea)
            arrowNum = 10;
    }

    public bool GetArea(){
        return inArea;
    }
}

总结

        上面就是游戏的主体功能的实现了。紧赶慢赶,前前后后用了一周的课余时间才做完的。可以看出游戏比较简陋,代码比较繁琐,而且不能动态地去加载所有的代码和资源;而且准心的实现也很简陋,只是在屏幕上简单的画了一个点等等,还有许多改进的地方。

        作业的完成参考了许多别人的博客,这里感谢所有愿意写博客分享技术的大佬们~

游戏的效果演示:

Unity游戏制作——射箭游戏

        后面应该会整理一下上传到GitHub

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值