23.2.17JohnLemon Haunt Jaunt项目学习

本文探讨3D游戏与2D游戏的区别,重点介绍了3D游戏中主角移动的实现,包括使用Quaternion表示旋转和音效处理。此外,还讲解了幽灵的WayPointPatrol移动机制,观察者系统如何检测玩家视野,以及游戏结束条件的判断。文章还涉及了相机抗锯齿、光照设置、后期处理效果如ColorGrading、Bloom和AmbientOcclusion,以及音频处理的不同策略。
摘要由CSDN通过智能技术生成

该项目回顾旨在说明3D游戏项目与2D游戏项目之间的不同,3D游戏具有2D游戏不具备的特性和需要注意的事项。

1.1主角移动PlayerMovement

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

public class PlayerMovement : MonoBehaviour
{

    Vector3 m_Movement;

    float horizontal;
    float vertical;

    Rigidbody m_rigidbody;

    Animator m_animator;

    //初始化旋转,四元数用来表示旋转
    Quaternion m_rotation = Quaternion.identity;
    //旋转速度
    public float turnSpeed = 20.0f;

    //声明音源组件
    AudioSource m_AudioSource;

    // Start is called before the first frame update
    void Start()
    {
        m_rigidbody = GetComponent<Rigidbody>();
        m_animator = GetComponent<Animator>();
        m_AudioSource = GetComponent<AudioSource>();
    }

    // Update is called once per frame
    void Update()
    {
        horizontal = Input.GetAxis("Horizontal");
        vertical = Input.GetAxis("Vertical");
    }

    private void FixedUpdate()
    {
        //用户输入组装为三维矢量
        m_Movement.Set(horizontal, 0.0f, vertical);
        m_Movement.Normalize();


        //判断是否有横向移动
        bool hasHorizontal = !Mathf.Approximately(horizontal, 0.0f);
        bool hasVertical = !Mathf.Approximately(vertical, 0.0f);
        bool isWalking = hasHorizontal || hasVertical;
        m_animator.SetBool("IsWalking", isWalking);

        //三维矢量表示转向后的角色
        Vector3 desiredForward = Vector3.RotateTowards(transform.forward, m_Movement, turnSpeed * Time.deltaTime, 0f);
        m_rotation = Quaternion.LookRotation(desiredForward);

        //如果走动,播放音效
        if (isWalking)
        {
            //保证不是每帧都重新播放!!
            if (!m_AudioSource.isPlaying)
            {
                m_AudioSource.Play();
            }
        }
        else
        {
            m_AudioSource.Stop();
        }
    }


    private void OnAnimatorMove()
    {
        //使用用户输入的三维矢量为移动方向,动画每0.02秒运动的距离为距离
        m_rigidbody.MovePosition(m_rigidbody.position + m_Movement * m_animator.deltaPosition.magnitude);
        m_rigidbody.MoveRotation(m_rotation);
    }
}

因为本次tutorial的内容是鬼屋探险,所以人物并未有太多可操作性,基本上就是基础的移动。需要注意的是,3D游戏中由于不能像2D游戏那样,通过角色动画表现方向,而是3D物体本身进行旋转表示方向,所以引入Quaternion四元数用来表示旋转。具体代码说明都在注释里。其中OnAnimatorMove()代表的是,每当动画产生了根运动时触发。由于根运动有自己的频率,人物移动速度也得和根运动速度相同,故采用delta的设置,使得0.02秒内的运动一致。

1.2幽灵移动WayPointPatrol

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

public class WayPointPatrol : MonoBehaviour
{

    NavMeshAgent navMeshAgent;
    public Transform[] waypoints;
    int m_CurrentWaypointIndex;

    // Start is called before the first frame update
    void Start()
    {
        navMeshAgent = GetComponent<NavMeshAgent>();
        navMeshAgent.SetDestination(waypoints[0].position);
    }

    //每次刷新,都要试着去获取下一个路径点
    //如果满足要求,指定下一个路径点
    //通过算法,让路径点位循环往复
    // Update is called once per frame
    void Update()
    {
        if (navMeshAgent.remainingDistance < navMeshAgent.stoppingDistance)
        {
            //获取下一个路径点在数组中的索引数
            m_CurrentWaypointIndex = (m_CurrentWaypointIndex + 1) % waypoints.Length;
            navMeshAgent.SetDestination(waypoints[m_CurrentWaypointIndex].position);
            //Debug.Log($"{gameObject.name} is going to {waypoints[m_CurrentWaypointIndex].position}!");
        }
    } 
}

在幽灵项目中,由于幽灵不会因被角色碰撞而改变自己的位置,所以在Rigidbody当中勾选Is Kinematic,如此一来就只会按照自己的运动学路径移动。

幽灵在本项目中用到了导航系统Nav Mesh。在Window-AI-Navigation中找到,选中地图Level组件,并且在Navigation的Bake烘培中更改参数,最后Bake后得到地图。

Attention:每次更改level的transform后,都要重新bake一遍地图,才能得到新的地图导航。

其中NavMeshAgent就是在地图中的控制组件,可以控制选中目的地。这里使用的是Transform组,用来遍历一个幽灵需要前往的不同地址,并且循环。其中stoppingDistance指的是在这个距离后就算到达,一般不设为0.0f,本项目设置为0.2f。

1.3静态敌人与动态敌人的捕捉视野Observer

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

public class Observer : MonoBehaviour
{
    public Transform player;
    bool m_IsPlayerInRange;
    public GameEnding gameEnding;


    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        if (m_IsPlayerInRange)
        {
            //设置投射射线用到的方向矢量
            Vector3 direction = player.position - transform.position + Vector3.up;
            Ray ray = new Ray(transform.position, direction);
            RaycastHit raycastHit;
            //若击中物体,则进入第一层if。第二个参数out代表输出参数,可以将射线射到的物体输出到变量raycastHit
            if (Physics.Raycast(ray,out raycastHit))
            {
                if (raycastHit.collider.transform == player)
                {
                    gameEnding.CaughtPlayer();
                }
            }
        }

    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.transform == player) 
        {
            m_IsPlayerInRange = true;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.transform == player)
        {
            m_IsPlayerInRange = false;
        }
    }
}

本方法在Update()中过于复杂,本可将游戏结束放在触发器事件中,进行离开设定是为了将m_IsPlayerInRange值改回false。但是可以将初始值放进Start()中,这样就不必进行繁琐的判断了。而且对于视线的设置更直观了,毕竟就是碰撞体本身。

1.4游戏结束

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

public class GameEnding : MonoBehaviour
{
    //声明开关变量,存储用户是否在触发器中
    bool m_IsPlayerAtExit;

    public GameObject player;

    //淡入淡出的时间,要有计时器
    public float fadeDuration = 1.0f;
    //计时器
    float fadeDurationTimer;
    //正常(完全不透明状态)显示结束UI的时间
    public float displayerImageDuration = 1.0f;

    //声明CanvasGroup,用来获取UI中的实例,来更改UI中图像的透明度
    public CanvasGroup exitBackgroundImageCanvasGroup;

    //新增一个表示游戏失败的结束界面UI
    public CanvasGroup caughtBackgroundImageCanvasGroup;
    bool m_IsPlayerCaught;


    //公开声明需要的音频文件,用以挂接
    public AudioSource exitAudio;
    public AudioSource caughtAudio;
    //因为两种声音只能播放一次,所以设定bool作为开关
    //默认false,播放过则true
    bool m_HasAudioPlayed;

    // Update is called once per frame
    void Update()
    {
        if (m_IsPlayerAtExit)
        {
            EndLevel(exitBackgroundImageCanvasGroup,false, exitAudio);
        }
        else if (m_IsPlayerCaught)
        {
            EndLevel(caughtBackgroundImageCanvasGroup, true, caughtAudio);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject == player)
        {
            m_IsPlayerAtExit = true;
        }
    }

    public void CaughtPlayer()
    {
        m_IsPlayerCaught = true;
    }


    //参数1:结束UI对应的游戏对象;参数2:是否重新开始
    private void EndLevel(CanvasGroup imageCanvasGroup, bool doRestart, AudioSource audioSource)
    {
        if (!m_HasAudioPlayed)
        {
            audioSource.Play();
            m_HasAudioPlayed = true;
        }


        fadeDurationTimer += Time.deltaTime;
        //alpha值控制透明度
        imageCanvasGroup.alpha = fadeDurationTimer / fadeDuration;


        //当计时器时长大于展示总时长,则退出游戏,故计时器随deltaTime增大 
        if (fadeDurationTimer > fadeDuration + displayerImageDuration)
        {
            if (doRestart)
            {
                SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
            }
            else
            {
                //退出当前应用程序
                //Application.Quit();

                UnityEditor.EditorApplication.isPlaying = false;
            }
        }
    }
}

游戏结束的组件中包含了两种结束方式,一种是AtExit,即通关逃脱,另一种是Caught,即被怪物抓住。分别挂接两个canvas group,一个是通关,一个是结束,都是canvas group下挂子对象image,且canvas group初始alpha值为0(1为不透明)。

该脚本有两种挂载形式,一种是直接挂在一个碰撞盒上,作为终点触发器结束游戏。另一种是挂在怪物的Observer上,作为玩家被Caught的结束方式。

二者都会叫出对应的UI,并且进行展示倒计时以及音效播放,展示通过更改Canvas group的alpha值渐增渐减决定。根据需求使得游戏获胜或者重新加载当前场景。

2.1相机抗锯齿

使用最简单的Main Camera配合Cinemachine。但注意在Main Camera上挂接Post-process Layer,选择设置的layer层。

抗锯齿Anti-aliasing,这里是用的是FXAA来节省性能。

延迟雾Deferred Fog也开启。

2.2光照

为了营造室内的鬼屋气氛,使用的是directional light 。具体配置在Windows>Rendering>Lighting中配置。将新的Light Setting存入文件New Lighting Setting中。

2.3后期处理

详细可见:Post Processing 后期处理_multi scale volumetric obscurance_童萌依然的博客-CSDN博客

创建新的空游戏对象,挂接Post-process Volume。并且设置新的layer,将Global Post的layer更改。

2.3.1 Color Grading色阶

Tonmapping mode - ACES为电影质感。

Post-exposure(EV) 曝光,大小为画面整体亮度。

Saturation饱和度。

Contrast对比度。

Lift, Gamma, Gain分别为暗部、灰部、亮部的调整。这里将暗部和灰部的色彩往蓝色冷色调整,就是为了营造鬼屋阴冷的效果。亮部往暖色调是为了将画面整体观感调回来,亮部也往往是灯光光源。

2.3.2 Bloom泛光效果

Intersity强度,泛光效果在屏幕上的作用区域。 

Treshold阈值,超过一定光亮度的光源才使用Bloom。

泛光效果是为了营造柔和光,基本用于高光、灯光,使得光线柔和。

2.3.3 Ambient Occlusion环境光遮挡

画面上不同地区由于Z-buffer有不同的深度,为了体现不同的深度,一个墙体在其边缘处一般有阴影,用来表现画面中物体的遮挡关系。

Intensity强度

Thickness Modifier 修改阴影的厚度

2.3.4 Vignette渐晕

渐晕为屏幕周围的晕影,这里由于是鬼屋项目,要体现阴暗的环境。

Intensity强度,这里体现晕影的总距离

Smoothness柔和度,这里体现的是淡出距离

2.3.5 Lens Distortion镜头失真

营造镜头透视,这里使用的是轻量的鱼眼透视。

Intensity镜头畸变强度

Scale镜头畸变大小

3.1音频(2D、3D)挂接

音频分为3d和2d。环境音、ui音、人物自己发出的声音(走路)都为2d音,不用立体音。因此将Audio Listener挂接到主角身上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值