*胡闹厨房*

 前期准备

详细教程

一、创建项目

1、选择Universal 3D,创建项目

2、删除预制文件Readme:点击Remove Readme Assets,弹出框上点击Proceed

3、Edit-Project Setting-Quality,只保留High Fidelity

4、打开 Assets-Settings ,保留URP-HighFidelity-Renderer 和 URP-HighFidelity 文件

5、新建Scripts、Prefabs、ScripatableObjects、Scripts/ScripatableObjects文件夹

二、导入资源

1、将资源拖入Project,在弹出框中点击Inport

2、重命名场景为GameScene

三、增加输入系统

1、安装Input System,注意:点击No

 2、设置输入系统

(1) Edit-Project Setting,选择Player-Other Settings,找到Configuration

(2) Active Input Handling 可选框,选择Both,弹出框选择Apply,Unity编辑器自动重启

3、添加输入事件:Settings文件夹下Create-Input Actions,命名为PlayerInputActions,双击打开

4、设置用键盘字母WASD的输入事件

(1) 添加输入事件:点击左上角 “+” 号命名为Player,将New action重命名为Move

(2) 更改Action Type和Control Type的值分别为 Value 和 Vector2

(3) 删除Move下的No Bingding,点击Move右侧 “+” 号,选择Add Up\Down\left\Right Composite

(4) 命名事件为WASD,点击Up…,点击Path可选框,在蓝色区域输入对应字母,选择Keyboard

(5) 点击 Save Asset

5、用键盘上的箭头控制的输入事件

(1) 点击Move右侧 “+” 号,选择Add Up\Down\left\Right Composite

(2) 命名事件为Arrow Keys,点击Up…,Path可选框,点击listen,输入对应箭头,选择Keyboard

(3) 点击 Save Asset

6、添加游戏手柄控制

(1) 电脑连接手柄,按动摇杆

(2) 选中Move,点击+号,弹出窗口选择Add Binding

(3) 点击Path右侧可选框,Joystick-listen,摇动摇杆,回车(或在Path中输入Left Stick)

(4) 点击 Save Asset

7、调整手柄控制

(1) 选中 Left Stick,点击Processors右侧加号,选择Stick Deadzone

(2) 取消勾选Min 右侧的Default,输入0.5

四、关联角色和输入事件

1、选中 Setting文件夹中的 PlayerInputActions

2、自动生成脚本:勾选Generate C# Class,点击Apply

后期效果处理

一、添加地板

1、3D Object-Plane,重命名为Floor,Reset它的Transform,Scale为5,5,5

2、设置材质:在Inspector面板下找到Mesh Renderer,设置Materials下的Element 0为Floor

二、虚拟相机

1、安装Cinemachine包

2、创建虚拟相机:GameObject-Cinemachine-Virtual Camera

3、设置虚拟相机

(1) Position:0.75,21.5,-20.79;Rotation为46,0,0

(2) Lens Vertical FOV:20

三、视觉效果

1、选中Settings文件夹下的URP-HighFidelity

(1) 设置Quality下的HDR为勾选状态,

(2) Anti Aliasing(MSAA)改为8x(抗锯齿)

2、设置URP-HighFidelity-Renderer下的Screen Space Ambient Occlusion(屏幕空间环境光遮蔽)

Intensity=4,Radius=0.3,DirectLighting Strength =1

3、创建新的Volume配置文件‌‌

(1) 方法:选中Global Volume,在Inspector面板找到Volume下的Profile,点击New

(2) 目的:用于定义和存储Volume的全局设置,包括各种后处理效果和其他渲染相关的参数。

(3) 作用:通过创建新的Profile,用户可以自定义和调整这些设置,以满足不同场景和渲染需求。

4、设置 Global Volume

(1) 点击Add Override,添加Tonemapping(颜色校正),勾选 Mode,选择Neutrual

(2) 点击Add Override,添加Color Adjustments:勾选Contrast,设为20;勾选Saturation,设为20

(3) 点击Add Override,添加Bloom(泛光):勾选Threshold,设为0.95;勾选Intensity,设为1

(4) 点击Add Override,添加Vignette(晕影):勾选Intensity,设为0.25;勾选Smoothness,0.4

四、背景音乐

1、Create Empy,命名为MusicManager,Reset Transform

2、为MusicManager添加Audio Source组件

(1) 设置Audio Source下的AudioClip为Music

(2) 勾选Play On Awake,勾选Loop(循环)

(3) Priority为0(播放优先级最高),Volume为0.5

3、确保Main Camera上有Audio Listener组件 

五、音效

1、Create Empy,命名为SoundManager,Reset Transform

2、音效对象

(1) 在Scripts/ScripatableObjects文件夹下新建AudioClipRefsSO.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class AudioClipRefsSO : ScriptableObject
{
    public AudioClip[] chop;
    public AudioClip[] deliveryFail;
    public AudioClip[] deliverySuccess;
    public AudioClip[] footstep;
    public AudioClip[] objectDrop;
    public AudioClip[] objectPickup;
    public AudioClip[] trash;
    public AudioClip[] warning;
    public AudioClip stoveSizzle;
}

(2) 在Assets文件夹新建ScriptableObjects文件夹

(3) ScriptableObjects文件夹下制作 AudioClipRefsSO对象,命名为AudioClipRefsSO,赋值

3、音效管理:为SoundManager添加SoundManager.cs

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

public class SoundManager : MonoBehaviour
{
    // 定义一个常量字符串,用于在PlayerPrefs中存储音效音量的键
    private const string PLAYER_PREFS_SOUND_EFFECTS_VOLUME = "SoundEffectsVolume";
    public static SoundManager Instance { get; private set; }
    [SerializeField] private AudioClipRefsSO audioClipRefsSO;
    private float volume = 1f;

    private void Awake()
    {
        Instance = this;
        // 从PlayerPrefs中读取音效音量,如果不存在则默认为1f
        volume = PlayerPrefs.GetFloat(PLAYER_PREFS_SOUND_EFFECTS_VOLUME, 1f);
    }
    private void PlaySound(AudioClip[] audioClipArray, Vector3 position, float volume = 1f)
    {
        if (audioClipArray != null && audioClipArray.Length > 0)
        {
            PlaySound(audioClipArray[Random.Range(0, audioClipArray.Length)], position, volume);
        }
        else
        {
            Debug.LogWarning("Audio clip array is null or empty.");
        }
    }
    private void PlaySound(AudioClip audioClip, Vector3 position, float volumeMultiplier = 1f)
    {
        AudioSource.PlayClipAtPoint(audioClip, position, volumeMultiplier * volume);
    }
    public void ChangeVolume(float amount = 0.1f)
    {
        volume += amount;
        if (volume > 1f)
        {
            volume = 0f;
        }
        PlayerPrefs.SetFloat(PLAYER_PREFS_SOUND_EFFECTS_VOLUME, volume);
        // PlayerPrefs.Save();
    }
    public float GetVolume()
    {
        return volume;
    }
}
 六、背景

1、围墙

(1) 3D-Cube Object,命名为Wall,Scale.x = 0.25,y=3,z=12.68,Position.x=8.4,y=1.37,z=-2.02

(2) 将Mesh Renderer下的Materials下的Element0 设置为_Assets/Materials中的Wall

(3) 复制Wall,Position.x=-6.9

(4) 复制Wall,Scale.z=15.53,Rotation.y= 90,Position.x=0.78,z=4.41

2、墙外

(1) 3D-Cube Object,Scale.x= 7.69,y=3.17,z=12.95,Position.x,y,z为12.36,1.27,-1.95,材质Black

(2) 复制Cube,Position.x=-10.87

(3) 复制Cube,Rotation.y= 90,Scale.z=31.5,Position.x=1.266,z=8.33

3、组织层级:选中三面墙和三块Cube,Create Empty Parent,命名为Walls

场景

一、定义场景

定义场景、设置加载场景的方法:新建Loader.cs

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

public class Loader
{
    public enum Scene { MainMenuScene, GameScene, LoadingScene, }
    private static Scene targetScene;
    public static void Load(Scene targetScene)
    {
        Loader.targetScene = targetScene;
        // 加载LoadingScene场景
        SceneManager.LoadScene(Scene.LoadingScene.ToString());
    }
    // 回调方法,用于从LoadingScene场景加载目标场景
    public static void LoaderCallback()
    {
        SceneManager.LoadScene(targetScene.ToString());
    }
}
二、开始场景

1、场景:在Scenes文件夹新建一个场景,命名为MainMenuScene

2、画布:打开MainMenuScene,UI-Canvas

(1) Canvas Scaler下的UI Scale Mode改为Scale With Screen Size

(2) Reference Resolution为1920 1080,Match为1,(完全匹配高度)

3、视觉效果:打开GameScene,复制Global Volume和Floor,粘贴到MainMenuScene中

4、设置镜头

(1) GameObject-CineMachine-Virtual Camera

(2) Pos.x=-0.51,y= 0.44,z=-1.74   Rotation.x = -16.638,y=18.495

(3) Noise选择Basic Multi Channel Perlin,Noise Profile选择Handheld_normal_mild

(4) Frequency Gain为0.5,Amplitude Gain为1

5、按钮:以Canvas为父物体,Create Empty,命名为MainMenuUI,Alt+拉伸

(1) 开始按钮

① 以MainMenuUI为父物体,UI-Button,命名为PlayButton,width = 450,Height= 150

② 选中PlayButton,锚定左下,Pos.x = 324  Pos.y = 336。按钮颜色3A3A3A

③ 给按钮添加Outline组件,Effect Color 中透明度255。Effect Distance下, x= 3  y= 3

④ 给按钮添加Shadow组件,Effect Distance下, x=5  y=-5

⑤ 展开PlayButton,文本内容为PLAY,加粗,字号70,白色

(2) 退出按钮

① 复制PlayButton,重命名为QuitButton。Pos.y = 127。Height= 120

② 展开QuitButton,文本内容为QUIT。字号58.7

(3) 设置按钮:给MainMenuUI添加MainMenuUI.cs组件

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

public class MainMenuUI : MonoBehaviour
{
    [SerializeField] private Button playButton;
    [SerializeField] private Button quitButton;

    private void Awake()
    {
        playButton.onClick.AddListener(() =>
        {
            Loader.Load(Loader.Scene.GameScene);
        });
        quitButton.onClick.AddListener(() =>
        {
            Application.Quit();
        });
        // 确保暂停之后重新游戏时游戏时间流速正常
        Time.timeScale = 1.0f;
    }
}

(4) 赋值

5、美化

(1) 导入角色

① 将_Assets/PrefabsVisuals中的PlayerViusal拖入场景:Pos.x=1.17,z=0.84,Rotation.y=-134

② 复制得到3个PlayerViusal,Pos.x=3.53,z=1.07,Rotation.y=-129

Pos.x=3.33,z=2.17,,Rotation.y=-136.6

Pos.x=0.71,z=2.27,Rotation.y=-153

③ 展开PlayerVisual (3),选中Head和Body,设置Materials下的Element 0为PlayerBody_Red,

④ 同样的方法设置PlayerVisual (3)的子物体head和body为蓝色,PlayerVisual (1)为绿色

(2) 添加图片

① 以 MainMenuUI 为父物体,UI-Image,Source Image为KitchenChaosLogo

② 锚点为左上,Width=881.72,Height=493.318,Pos.X=492,Y=-307

三、加载场景

1、场景:在Scenes文件夹新建一个场景,命名为LoadingScene

2、相机背景

(1) 选中Main Camera,Inspetor面板展开Camera-Environment

(2) Background Type为Solid Color,Background为黑色

3、画布:UI-Canvas

(1) Canvas Scaler下的UI Scale Mode改为Scale With Screen Size

(2) Reference Resolution为1920 1080,Match为1,(完全匹配高度)

4、文本:

(1) UI-Text,Pos.X= -883,Pos.Y= -418。宽高 都为0

(2) 文本内容:LOADING...  文字加粗、字号55,不换行,白色

5、切换场景

(1) 目的:LoadingScene场景加载完毕后调用Loader类中的回调方法(进入游戏场景GameScene)

(2) Create Empty,命名为LoaderCallback

(3) 为LoaderCallback添加LoaderCallback.cs组件

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

public class LoaderCallback : MonoBehaviour
{
    private bool isFirstUpdate = true;
    private void Update()
    {
        if (isFirstUpdate)
        {
            isFirstUpdate = false;
            Loader.LoaderCallback();
        }
    }
}

角色控制器

一、创建角色

1、打开GameScene,Create Empty,命名为Player,重置Transform

2、将PlayerVisual作为Player的子物体拖放到Hierarchy面板上,重置Transform

3、以Player为父物体,Create Empty,命名为KitchenObjectHoldPoint,position.y = 1.456,z=1

二、角色运动

1、获取玩家的输入

(1) Create Empty,命名为GameInput,用于承载处理输入的逻辑。重置Transform

(2) 获取玩家输入并将其转换为一个归一化的 Vector2 向量:为GameInput 添加GameInput.cs

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

public class GameInput : MonoBehaviour
{
    private PlayerInputActions playerInputActions;
    private void Awake()
    {
        playerInputActions = new PlayerInputActions();
        //调用 PlayerInputActions 类中的 Player 动作组的 Enable 方法,使玩家输入生效
        playerInputActions.Player.Enable();
    }
    public Vector2 GetMovementVectorNormalized()
    {
        Vector2 inputVector = playerInputActions.Player.Move.ReadValue<Vector2>();
        inputVector = inputVector.normalized;
        Debug.Log(inputVector);
        return inputVector;
    }
}

2、角色移动和旋转 :

(1) 为Player添加Player.cs组件

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

public class Player : MonoBehaviour
{
    public static Player Instance { get; private set; }

    [SerializeField] private float moveSpeed = 7.0f;
    [SerializeField] private GameInput gameInput;

    private bool isWalking;
    private void Awake()
    {
        if (Instance != null)
        {
            Debug.LogError("There is more than one Player instance");
        }
        Instance = this;
    }
    private void Update()
    {
        HandleMovement();
    }

    public bool IsWalking()
    {
        return isWalking;
    }
    private void HandleMovement()
    {
        Vector2 inputVector = gameInput.GetMovementVectorNormalized();
        Vector3 moveDir = new(inputVector.x, 0f, inputVector.y);

        transform.position += moveDir * moveSpeed * Time.deltaTime;
        // 判断moveDir是否不等于0向量,再将判断结果赋值给isWalking
        isWalking = moveDir != Vector3.zero;
        float rotationSpeed = 10f;
        transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);
    }
}

(2) 赋值

3、根据玩家的行走状态实时更新动画,关联动画与行走状态

(1) 为PlayerVisual添加PlayerAnimator.cs组件

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

public class PlayerAnimator : MonoBehaviour
{
    private const string IS_WALKING = "IsWalking";
    [SerializeField] private Player player;

    private Animator animator;

    private void Awake()
    {
        animator = GetComponent<Animator>();
    }
    private void Update()
    {
        animator.SetBool(IS_WALKING, player.IsWalking());
    }
}

(2) 赋值

3、角色移动粒子特效

(1) 将_Assets/PrefabsVisuals中的PlayerMovingParticles作为Player的子物体

(2) Rotation.x=-90

四、角色移动音效

1、脚步音效的方法:编辑SoundManager.cs

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

public class SoundManager : MonoBehaviour
{
    private const string PLAYER_PREFS_SOUND_EFFECTS_VOLUME = "SoundEffectsVolume";
    public static SoundManager Instance { get; private set; }
    [SerializeField] private AudioClipRefsSO audioClipRefsSO;
    private float volume = 1f;

    private void Awake()
    {
        Instance = this;
        volume = PlayerPrefs.GetFloat(PLAYER_PREFS_SOUND_EFFECTS_VOLUME, 1f);
    }
    // 脚步音效
    public void PlayFootstepSound(Vector3 position,float volume)
    {
        PlaySound(audioClipRefsSO.footstep,position,volume);
    }
    private void PlaySound(AudioClip[] audioClipArray, Vector3 position, float volume = 1f)
    {
        if (audioClipArray != null && audioClipArray.Length > 0)
        {
            PlaySound(audioClipArray[Random.Range(0, audioClipArray.Length)], position, volume);
        }
        else
        {
            Debug.LogWarning("Audio clip array is null or empty.");
        }
    }
    private void PlaySound(AudioClip audioClip, Vector3 position, float volumeMultiplier = 1f)
    {
        AudioSource.PlayClipAtPoint(audioClip, position, volumeMultiplier * volume);
    }
    public void ChangeVolume(float amount = 0.1f)
    {
        volume += amount;
        if (volume > 1f)
        {
            volume = 0f;
        }
        PlayerPrefs.SetFloat(PLAYER_PREFS_SOUND_EFFECTS_VOLUME, volume);
        // PlayerPrefs.Save();
    }
    public float GetVolume()
    {
        return volume;
    }
}

2、调用音效:给Player添加PlayerSounds.cs组件

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

public class PlayerSounds : MonoBehaviour
{
    private Player player;
    private float footstepTimer;
    private float footstepTimerMax = .1f;
    private void Awake()
    {
        player = GetComponent<Player>();
    }
    private void Update()
    {
        footstepTimer -= Time.deltaTime;
        if(footstepTimer < 0f)
        {
            footstepTimer = footstepTimerMax;
            if (player.IsWalking())
            {
                float volume = 1f;
                SoundManager.Instance.PlayFootstepSound(player.transform.position, volume);
            }
        }
    }
}
五、碰撞检测

1、遇到障碍物后改变方向:编辑Player.cs

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

public class Player : MonoBehaviour
{
    public static Player Instance { get; private set; }

    [SerializeField] private float moveSpeed = 7.0f;
    [SerializeField] private GameInput gameInput;

    private bool isWalking;
    private void Awake()
    {
        if (Instance != null)
        {
            Debug.LogError("There is more than one Player instance");
        }
        Instance = this;
    }
    private void Update()
    {
        HandleMovement();
    }

    public bool IsWalking()
    {
        return isWalking;
    }
    private void HandleMovement()
    {
        Vector2 inputVector = gameInput.GetMovementVectorNormalized();
        Vector3 moveDir = new(inputVector.x, 0f, inputVector.y);

        float moveDistance = moveSpeed * Time.deltaTime;
        float playerRadius = 0.7f;
        float playerHeight = 2f;
        // 判断是否发生碰撞,若发生,canMove的值为false;否则为true(未碰撞时角色可移动)
        bool canMove = !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDir, moveDistance);
        
        // 碰撞检测和尝试沿不同方向移动
        // 如果 !canMove的值为true(即如果发生碰撞,则canMove为false,!canMove就为true)
        if (!canMove)
        {
            // 若发生碰撞,尝试沿x轴方向移动
            Vector3 moveDirX = new Vector3(moveDir.x, 0f, 0f).normalized;
            // 检查沿X轴方向移动是否可行,并且没有碰撞
            // 确保只有当玩家确实想要在 x 轴方向上移动(即不是微小的、几乎可以忽略不计的移动)时,才进行碰撞检测。
            canMove = (moveDir.x < -.5f || moveDir.x > +0.5f) && !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirX, moveDistance);
            if (canMove)
            {
                // 如果可以沿X轴移动,则更新移动方向
                moveDir = moveDirX;
            }
            else
            {
                // 如果不能沿X轴移动,尝试沿Z轴方向移动
                Vector3 moveDirZ = new Vector3(0f, 0f, moveDir.z).normalized;
                canMove = (moveDir.z < -.5f || moveDir.z > 0.5f) && !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);
                if (canMove)
                {
                    moveDir = moveDirZ;
                }
                else
                {
                    // 不能向任何方向移动
                }
            }
        }
        // 确定可以移动时更新角色的位置
        // 如果最终确定可以移动(无论是沿X轴还是Z轴)
        if (canMove)
        {
            transform.position += moveDir * moveDistance;
        }
        isWalking = moveDir != Vector3.zero;
        float rotationSpeed = 10f;
        transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);
    }
}
六、角色交互

1、创建测试用柜台

(1) 在Prefabs文件夹下,新建文件夹,命名为Counters

(2) Create Empty ,命名为_BaseCounter,Transform.Position为0,0,2.71,Rotation.y=-180

(3) 添加并设置 Box Collider组件,设置Center.y=0.5,Size为1.5,1.5,1.5

(4) 为_BaseCounter添加 Counters 图层,选择No,this object only

(5) 为_BaseCounter添加子物体

① Create Empty,命名为 CounterTopPoint

② 设置CounterTopPoint 的Transform,Position为0,1.3,0

(6) 在Scripts/Counters文件夹下,新建BaseCounter.cs(空内容)

(7) 将_BaseCounter制成预制体

(8) 制作_BaseCounter预制体变体,命名为ClearCounter,添加ClearCounter.cs组件

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

public class ClearCounter : BaseCounter
{

}

① 选择ClearCounter_Visual作为Counter的子物体

② 复制ClearCounter_Visual,重命名为Selected,设置它的Scale为1.01,1.01,1.01

③ 选中Selected的子物体 KitchenCounter,更改材质为CounterSelected(设置灰度)

④ 禁用(隐藏)KitchenCounter

2、选中效果

(1) 向场景添加ClearCounter,Position为7.5,0,3.5;Rotation.y=-180

(2) 声明、触发委托,增加交互方法:编辑Player.cs

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

public class Player : MonoBehaviour
{
    public static Player Instance { get; private set; }

    public event EventHandler<OnSelectedCounterChangedEventArgs> OnSelectedCounterChanged;
    public class OnSelectedCounterChangedEventArgs : EventArgs
    {
        public BaseCounter SelectedCounter { get; }
        public OnSelectedCounterChangedEventArgs(BaseCounter selectedCounter)
        {
            SelectedCounter = selectedCounter;
        }
    }

    [SerializeField] private float moveSpeed = 7.0f;
    [SerializeField] private GameInput gameInput;
    [SerializeField] private LayerMask countersLayerMask;

    private bool isWalking;
    private Vector3 lastInteractDir;
    private BaseCounter selectedCounter;
    private void Awake()
    {
        if (Instance != null)
        {
            Debug.LogError("There is more than one Player instance");
        }
        Instance = this;
    }
    private void Update()
    {
        HandleMovement();
        HandleInteractions();
    }

    public bool IsWalking()
    {
        return isWalking;
    }

    private void HandleInteractions()
    {
        Vector2 inputVector = gameInput.GetMovementVectorNormalized();
        Vector3 moveDir = new Vector3(inputVector.x, 0f, inputVector.y);

        if (moveDir != Vector3.zero)
        {
            lastInteractDir = moveDir;
        }

        float interactDistance = 2f;
        if (Physics.Raycast(transform.position, lastInteractDir, out RaycastHit raycastHit, interactDistance, countersLayerMask))
        {
            if (raycastHit.transform.TryGetComponent(out BaseCounter baseCounter))
            {
                // Has ClearCounter
                if (baseCounter != selectedCounter)
                {
                    SetSelectedCounter(baseCounter);
                }
            }
            else
            {
                SetSelectedCounter(null);
            }
        }
        else
        {
            SetSelectedCounter(null);
        }
    }
    private void HandleMovement()
    {
        Vector2 inputVector = gameInput.GetMovementVectorNormalized();
        Vector3 moveDir = new(inputVector.x, 0f, inputVector.y);

        float moveDistance = moveSpeed * Time.deltaTime;
        float playerRadius = 0.7f;
        float playerHeight = 2f;

        bool canMove = !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDir, moveDistance);

        if (!canMove)
        {
            Vector3 moveDirX = new Vector3(moveDir.x, 0f, 0f).normalized;

            canMove = (moveDir.x < -.5f || moveDir.x > +0.5f) && !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirX, moveDistance);
            if (canMove)
            {
                moveDir = moveDirX;
            }
            else
            {
                Vector3 moveDirZ = new Vector3(0f, 0f, moveDir.z).normalized;
                canMove = (moveDir.z < -.5f || moveDir.z > 0.5f) && !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);
                if (canMove)
                {
                    moveDir = moveDirZ;
                }
                else
                {
                    // 不能向任何方向移动
                }
            }
        }
        if (canMove)
        {
            transform.position += moveDir * moveDistance;
        }
        isWalking = moveDir != Vector3.zero;
        float rotationSpeed = 10f;
        transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);
    }
    private void SetSelectedCounter(BaseCounter selectedCounter)
    {
        this.selectedCounter = selectedCounter;
        OnSelectedCounterChanged?.Invoke(this,new OnSelectedCounterChangedEventArgs(selectedCounter));
    }
}

(3) 赋值:图层

(4) 为Selected 添加 SelectedCounterVisual.cs

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

public class SelectedCounterVisual : MonoBehaviour
{
    [SerializeField] private BaseCounter baseCounter;
    [SerializeField] private GameObject[] visualGameObjectArray;

    private void Start()
    {
        Player.Instance.OnSelectedCounterChanged += Player_OnSelectedCounterChanged;
    }

    private void Player_OnSelectedCounterChanged(object sender, Player.OnSelectedCounterChangedEventArgs e)
    {
        if (e.SelectedCounter == baseCounter)
        {
            Show();
        }
        else
        {
            Hide();
        }
    }

    private void Show()
    {
        foreach (GameObject visualGameObject in visualGameObjectArray)
        {
            visualGameObject.SetActive(true);
        }
    }

    private void Hide()
    {
        foreach (GameObject visualGameObject in visualGameObjectArray)
        {
            visualGameObject.SetActive(false);
        }
    }
}

食材和盘子

一、食材预制体

1、新建Prefabs文件夹,在Prefabs文件夹下,新建文件夹命名为 KitchenObjects

2、制作番茄预制体

(1) Create Empty,命名为Tomato,设置Transform,Position为 -2.16,0,0.75

(2) 添加子物体Tomato_Visual

(3) 将Tomato制成预制体,放入KitchenObjects文件夹。

(4) 删除Hierarchy面板上的Tomato

3、制作CheeseBlock预制体

(1) 复制预制体Tomato,重命名(F2)为CheeseBlock

(2) 打开CheeseBlock预制体

(3) 添加子物体 CheeseBlock_Visual,删除Tomato_Visual

4、同样的方法制作Bread、Cabbage、MeatPattyUncooked、MeatPattyCooked 预制体

5、番茄切片

(1) 复制Tomato 预制体,重命名为TomatoSlices

(2) 编辑TomatoSlices预制体:添加TomatoSlices,删除Tomato

6、同样的方法制作 CheeseSlices、CabbageSlices、MeatPattyBurned、Plate预制体

二、食材和盘子
2.1 食材

1、新建ScriptableObjects文件夹,其下新建KitchenObjectSO文件夹

2、食材信息管理:

(1) 在Scripts文件夹下新建 ScriptableObjects文件夹

(2) 在Scripts/ ScriptableObjects文件夹下新建 KitchenObjectSO.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class KitchenObjectSO : ScriptableObject
{
    public Transform prefab;
    public Sprite sprite;
    public string objectName;
}

3、创建并设置各食材

(1) 在KitchenObjectSO文件夹下,新建 Kitchen Object SO 文件,命名为Tomato

(2) 新建 Kitchen Object SO 文件,命名为TomatoSlices

(3) 创建kitchenObjectSO对象:CheeseBlock、Bread、Cabbage、MeatPattyUncooked

(4) 创建:CheeseSlices、CabbageSlices、MeatPattyCooked、MeatPattyBurned、Plate

4、关联食材与食材属性:

(1) 为除 Plate 外的所有食材预制体添加KitchenObject.cs

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

public class KitchenObject : MonoBehaviour
{
    [SerializeField] private KitchenObjectSO kitchenObjectSO;
    public KitchenObjectSO GetKitchenObjectSO()
    {
        return kitchenObjectSO;
    }
}

(2) 分别赋值

2.2 盘子

1、设置可装盘的食材列表:

(1) 为 Plate 预制体添加PlateKitchenObject.cs组件 

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

public class PlateKitchenObject : KitchenObject
{
    public event EventHandler<OnIngredientAddedEventArgs> OnIngredientAdded;
    public class OnIngredientAddedEventArgs : EventArgs
    {
        public KitchenObjectSO kitchenObjectSO;
    }
    // 可装盘的食材列表 
    [SerializeField] private List<KitchenObjectSO> validKitchenObjectSOList;
    // 食材列表
    private List<KitchenObjectSO> kitchenObjectSOList;

    private void Awake()
    {
        kitchenObjectSOList = new List<KitchenObjectSO>();
    }
    // 尝试将食材添加到食材列表
    public bool TryAddIngredient(KitchenObjectSO kitchenObjectSO)
    {
        // 如果待添加的食材不在可装盘的食材列表中
        if (!validKitchenObjectSOList.Contains(kitchenObjectSO))
        {
            // Not a valid ingredient
            return false;// 添加失败
        }
        // 如果食材列表中已经包含待添加的食材
        if (kitchenObjectSOList.Contains(kitchenObjectSO))
        {
            // Already has this type
            return false;
        }
        else
        {
            kitchenObjectSOList.Add(kitchenObjectSO);

            OnIngredientAdded?.Invoke(this, new OnIngredientAddedEventArgs
            {
                kitchenObjectSO = kitchenObjectSO
            });

            return true;
        }
    }
    // 获取当前盘子中实际摆放的食材的列表
    public List<KitchenObjectSO> GetKitchenObjectSOList()
    {
        return kitchenObjectSOList;
    }
}

 (3) 赋值

2、盘中物品显示:编辑 Plate 预制体

(1) 将_Assets/PrefabsVisuals文件夹下的PlateCompleteVisual预制体作为子物体添加到Plate上

(2) 订阅委托:给PlateCompleteVisual添加PlateCompleteVisual.cs组件

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

public class PlateCompleteVisual : MonoBehaviour
{
    [Serializable]
    public struct KitchenObjectSO_GameObject
    {
        public KitchenObjectSO kitchenObjectSO; // 存储食材的数据
        public GameObject gameObject; // 该食材在场景中的实例
    }

    [SerializeField] private PlateKitchenObject plateKitchenObject;
    // 存储食材对象设定和食材实例化物品的对应关系。
    // 指定盘子可以承载的所有食材及其对应的GameObject
    [SerializeField] private List<KitchenObjectSO_GameObject> kitchenObjectSOGameObjectList;

    private void Start()
    {
        plateKitchenObject.OnIngredientAdded += PlateKitchenObject_OnIngredientAdded;

        foreach (KitchenObjectSO_GameObject kitchenObjectSOGameObject in kitchenObjectSOGameObjectList)
        {
            kitchenObjectSOGameObject.gameObject.SetActive(false);
        }
    }

    private void PlateKitchenObject_OnIngredientAdded(object sender, PlateKitchenObject.OnIngredientAddedEventArgs e)
    {
        foreach (KitchenObjectSO_GameObject kitchenObjectSOGameObject in kitchenObjectSOGameObjectList)
        {
            if (kitchenObjectSOGameObject.kitchenObjectSO == e.kitchenObjectSO)
            {
                kitchenObjectSOGameObject.gameObject.SetActive(true);
            }
        }
    }
}

(3) 赋值(注意:使用PlateCompleteVisual的子物体)

3、显示盘中食材图标

(1) 在Package Manager中安装 2D Sprite

(2) 设置 图标 UI

① 打开Plate预制体,新建一个Canvas重命名为PlateIconsUI

② 更改Canvas的Render Mode为World Space,Pos.x,z,都为0,宽,高都为0.9,Pos.y = 1

③ Create Empty 作为PlateIconsUI的子物体,命名为IconTemplate,宽高都为0.3

④ 新建 IconTemplate的子物体UI - Image,命名为Background,拉伸

⑤ 设置Image的Source Image:注意点击右上角眼睛图标(显示隐藏)后,搜索circle

⑥ 复制Background,重命名为Icon,设置Image的Source Image为Bread

⑦ Hierarchy的层次如下图

⑧ PlateIconsUI添加Grid Layout Group组件,Cell Size0.3,0.3,Child Alignment为Middle Center

(3) 某一个食材的图标

① 给IconTemplate添加PlateIconsSingleUI.cs组件 

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

public class PlateIconsSingleUI : MonoBehaviour
{
    [SerializeField] private Image image;
    public void SetKitchenObjectSO(KitchenObjectSO kitchenObjectSO)
    {
        image.sprite = kitchenObjectSO.sprite;
    }
}

② 赋值

(4) 显示图标

① 给PlateIconsUI添加PlateIconsUI.cs组件

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

public class PlateIconsUI : MonoBehaviour
{
    [SerializeField] private PlateKitchenObject plateKitchenObject;
    [SerializeField] private Transform iconTemplate;

    private void Awake()
    {
        iconTemplate.gameObject.SetActive(false);
    }

    private void Start()
    {
        // 当有新成分被添加到盘子时,调用 PlateKitchenObject_OnIngredienAdded 方法
        plateKitchenObject.OnIngredientAdded += PlateKitchenObject_OnIngredientAdded;
    }

    private void PlateKitchenObject_OnIngredientAdded(object sender, PlateKitchenObject.OnIngredientAddedEventArgs e)
    {
        UpdateVisual();
    }

    private void UpdateVisual()
    {
        foreach (Transform child in transform)
        {
            // 如果子物体是iconTemplate,跳过当前循环迭代,继续下一个循环迭代
            if (child == iconTemplate) continue;
            Destroy(child.gameObject);
        }

        foreach (KitchenObjectSO kitchenObjectSO in plateKitchenObject.GetKitchenObjectSOList())
        {
            Transform iconTransform = Instantiate(iconTemplate, transform);
            iconTransform.gameObject.SetActive(true);
            iconTransform.GetComponent<PlateIconsSingleUI>().SetKitchenObjectSO(kitchenObjectSO);
        }
    }
}

② 赋值

(5) 改变图标朝向:子物体 PlateIconsUI 添加LookAtCamera.cs组件,Mode改为Camera Forward

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

public class LookAtCamera : MonoBehaviour
{
    private enum Mode { LootAt, LookAtInverted, CameraForward, CameraForwardInverted, }
    [SerializeField] private Mode mode;
    private void LateUpdate()
    {
        switch (mode)
        {
            case Mode.LootAt:
                transform.LookAt(Camera.main.transform);
                break;
            case Mode.LookAtInverted:
                Vector3 dirFromCamera = transform.position - Camera.main.transform.position;
                transform.LookAt(transform.position + dirFromCamera);
                break;
            case Mode.CameraForward:
                transform.forward = Camera.main.transform.forward;
                break;
            case Mode.CameraForwardInverted:
                transform.forward = -Camera.main.transform.forward;
                break;
        }
    }
}

5、关联食材与食材切片

(1) 在Scripts/ ScriptableObjects文件夹下新建 CuttingRecipeSO.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class CuttingRecipeSO : ScriptableObject
{
    public KitchenObjectSO input;
    public KitchenObjectSO output;
    public int cuttingProgressMax;
}

(2) 在ScriptableObjects下新建 CuttingRecipeSO文件夹

(3) 在CuttingRecipeSO文件夹下创建CuttingRecipeSO对象,命名为Tomato-TomatoSilices赋值

(4) 分别创建和设置:Cabbage-CabbageSlices(max=5)、CheeseBlock-CheeseSlices(3)

9、关联生、熟肉饼

(1) 在Scripts/ ScriptableObjects文件夹下新建FryingRecipeSO.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class FryingRecipeSO : ScriptableObject
{
    public KitchenObjectSO input;
    public KitchenObjectSO output;
    public float fryingTimerMax;
}

(2) 在ScriptableObjets文件夹下新建FryingRecipeSO文件夹

(3) 新建FryingRecipeSO对象,命名为MeatPattyUncooked-MeatPattyCooked,Max=5

10、关联熟、焦肉饼

(1) 在Scripts/ScriptableObjects文件夹,新建BurningRecipeSO.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu()]
public class BurningRecipeSO : ScriptableObject
{
    public KitchenObjectSO input;
    public KitchenObjectSO output;
    public float burningTimerMax;
}

(2) 在ScriptableObjects下新建BurningRecipeSO文件夹

(3) 创建BurningRecipeSO对象:新建MeatPattyCooked-MeatPattyBurned,Max=6

橱柜

一、橱柜总属性

1、在Scripts文件夹下新建 Interface 文件夹

2、创建接口,用于定义食材的父对象(厨柜、玩家)的行为:新建 IkitchenObjectParent.cs

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

public interface IkitchenObjectParent
{
    //获取食材的Transform
    public Transform GetKitchenObjectFollowTransform();
    //设置食材
    public void SetKitchenObject(KitchenObject kitchenObject);
    //拿取食材
    public KitchenObject GetKitchenObject();
    //清除食材
    public void ClearKitchenObject();
    //检查是否有食材
    public bool HasKitchenObject();
}

3、实现接口

(1) 编辑BaseCounter.cs ,用于处理橱柜的基本行为和交互

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

public class BaseCounter : MonoBehaviour, IKitchenObjectParent
{
    [SerializeField] private Transform counterTopPoint;
    private KitchenObject kitchenObject;

    public virtual void Interact(Player player)
    {
        Debug.LogError("BaseCounter.Interact();");
    }

    public virtual void InteractAlternate(Player player)
    {
        //Debug.LogError("BaseCounter.InteractAlternate();");
    }
    public Transform GetKitchenObjectFollowTransform()
    {
        return counterTopPoint;
    }
    public void SetKitchenObject(KitchenObject kitchenObject)
    {
        this.kitchenObject = kitchenObject;
    }

    public KitchenObject GetKitchenObject()
    {
        retu

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/960384.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习】线性回归的简洁实现

线性回归的简洁实现 在过去的几年里&#xff0c;出于对深度学习强烈的兴趣&#xff0c;许多公司、学者和业余爱好者开发了各种成熟的开源框架。 这些框架可以自动化基于梯度的学习算法中重复性的工作。 目前&#xff0c;我们只会运用&#xff1a; &#xff08;1&#xff09;通…

Java 网络原理 ②-IP协议

这里是Themberfue 经过五节课的传输层协议的讲解&#xff0c;接下来我们将进入网络层协议——IP协议的讲解了~~~ IP协议 IP 相信大家在日常生活中或多或少都听过&#xff0c;你的IP地址是什么&#xff1f;192.168.0.1 ......✨IP 其实是个网络层协议&#xff0c;即互联网协议&…

PETSc源码分析:Nonlinear Solvers

本文结合PETSc源代码&#xff0c;总结PETSc中的非线性方程组求解器。 注1&#xff1a;限于研究水平&#xff0c;分析难免不当&#xff0c;欢迎批评指正。 注2&#xff1a;文章内容会不定期更新。 参考文献 Balay S. PETSc/TAO Users Manual, Revision 3.22. Argonne National …

嵌入式C语言:结构体的多态性之结构体中的void*万能指针

目录 一、void*指针在结构体中的应用 二、实现方式 2.1. 定义通用结构体 2.2. 定义具体结构体 2.3. 初始化和使用 三、应用场景 3.1. 内存管理函数 3.2. 泛型数据结构&#xff08;链表&#xff09; 3.3. 回调函数和函数指针 3.4. 跨语言调用或API接口&#xff08;模拟…

反向代理模块。。

1 概念 1.1 反向代理概念 反向代理是指以代理服务器来接收客户端的请求&#xff0c;然后将请求转发给内部网络上的服务器&#xff0c;将从服务器上得到的结果返回给客户端&#xff0c;此时代理服务器对外表现为一个反向代理服务器。 对于客户端来说&#xff0c;反向代理就相当于…

构建旧系统:打造可维护系统的艺术

作者&#xff1a;来自 Elastic Saman Nourkhalaj 软件开发人员有很多不同的任务&#xff0c;但我们每个人都必须审查旧代码。无论是检查以前的版本还是查看过去某人如何解决问题&#xff0c;遗留代码都是工作的一部分。但是你是否曾经审查过以前的版本并感到沮丧并问 “谁编写了…

PAT (Basic Level) Practice 乙级1031-1040

制作不易&#xff0c;大家的点赞和关注就是我更新的动力&#xff01; 由于这些题全是大一寒假刷的&#xff0c;部分还是c语言&#xff0c;部分的解题方法比较复杂&#xff0c;希望大家体谅。有问题欢迎大家在评论区讨论&#xff0c;有不足也请大家指出&#xff0c;谢谢大家&am…

BUUCTF 蜘蛛侠呀 1

BUUCTF:https://buuoj.cn/challenges 文章目录 题目描述&#xff1a;密文&#xff1a;解题思路&#xff1a;flag&#xff1a; 相关阅读 CTF Wiki Hello CTF NewStar CTF buuctf-蜘蛛侠呀 BUUCTF&#xff1a;蜘蛛侠呀 MISC&#xff08;时间隐写&#xff09;蜘蛛侠呀 题目描述&am…

面向长文本的多模型协作摘要架构:多LLM文本摘要方法

多LLM摘要框架在每轮对话中包含两个基本步骤:生成和评估。这些步骤在多LLM分散式摘要和集中式摘要中有所不同。在两种策略中,k个不同的LLM都会生成多样化的文本摘要。然而在评估阶段,多LLM集中式摘要方法使用单个LLM来评估摘要并选择最佳摘要,而分散式多LLM摘要则使用k个LLM进行…

c语言版贪吃蛇(Pro Max版)附源代码

1 背景 贪吃蛇是一款经典的电子游戏&#xff0c;最早出现在20世纪70年代的街机游戏中。游戏的核心玩法是玩家控制一条蛇在有限的空间内移动&#xff0c;通过吃食物来增长身体长度&#xff0c;同时避免撞到墙壁、障碍物或自身。随着蛇的长度增加&#xff0c;游戏难度逐渐提升。 …

AI软件外包需要注意什么 外包开发AI软件的关键因素是什么 如何选择AI外包开发语言

1. 定义目标与需求 首先&#xff0c;要明确你希望AI智能体做什么。是自动化任务、数据分析、自然语言处理&#xff0c;还是其他功能&#xff1f;明确目标可以帮助你选择合适的技术和方法。 2. 选择开发平台与工具 开发AI智能体的软件时&#xff0c;你需要选择适合的编程语言、…

分布式理解

分布式 如何理解分布式 狭义的分布是指&#xff0c;指多台PC在地理位置上分布在不同的地方。 分布式系统 分布式系**统&#xff1a;**多个能独立运行的计算机&#xff08;称为结点&#xff09;组成。各个结点利用计算机网络进行信息传递&#xff0c;从而实现共同的“目标或者任…

python学opencv|读取图像(四十七)使用cv2.bitwise_not()函数实现图像按位取反运算

【0】基础定义 按位与运算&#xff1a;两个等长度二进制数上下对齐&#xff0c;全1取1&#xff0c;其余取0。按位或运算&#xff1a;两个等长度二进制数上下对齐&#xff0c;有1取1&#xff0c;其余取0。 按位取反运算&#xff1a;一个二进制数&#xff0c;0变1,1变0。 【1】…

CVE-2023-38831 漏洞复现:win10 压缩包挂马攻击剖析

目录 前言 漏洞介绍 漏洞原理 产生条件 影响范围 防御措施 复现步骤 环境准备 具体操作 前言 在网络安全这片没有硝烟的战场上&#xff0c;新型漏洞如同隐匿的暗箭&#xff0c;时刻威胁着我们的数字生活。其中&#xff0c;CVE - 2023 - 38831 这个关联 Win10 压缩包挂…

链表排序--(奇数位是升序,偶数位是降序)

题目描述 对一个单链表进行排序&#xff0c;但这个链表有一个特殊的结构&#xff1a; 奇数位是升序&#xff1a;链表中位于奇数位置的节点是按升序排列的。例如&#xff0c;如果链表的第1个节点的值是1&#xff0c;第3个节点的值是3&#xff0c;第5个节点的值是5&#xff0c;那…

在无sudo权限Linux上安装 Ollama 并使用 DeepSeek-R1 模型

本教程将指导你如何在 Linux 系统上安装 Ollama&#xff08;一个本地运行大型语言模型的工具&#xff09;&#xff0c;并加载 DeepSeek-R1 模型。DeepSeek-R1 是一个高性能的开源语言模型&#xff0c;适用于多种自然语言处理任务。 DeepSeek-R1 简介 DeepSeek-R1 是 DeepSeek …

arduino学习

一、log日志 只看自己 看指定 看错误日志 二、布局 重要&#xff1a;新建activity时需要的配置 若一个工程中有多个activity&#xff0c;需要修改开启activity属性、总容器标签、debug启动activity。下面流程内截图activity不一致&#xff0c;根据自己新建的activity配置&am…

obsidian插件——Metadata Hider

原本是要找导出图片时显示属性的插件&#xff0c;奈何还没找到&#xff0c;反而找到了可以隐藏属性的插件。唉&#xff0c;人生不如意&#xff0c;十之八九。 说一下功能&#xff1a; 这个插件可以把obsidian的文档属性放在右侧显示&#xff0c;或者决定只显示具体几项属性&a…

SimpleFOC STM32教程10|基于STM32F103+CubeMX,速度闭环控制(有电流环)

导言 SimpleFOC STM32教程09&#xff5c;基于STM32F103CubeMX&#xff0c;ADC采样相电流 如上图所示, 增加了电流环. 效果如下&#xff1a; 20250123-200906 RTT 如上图所示&#xff0c;三相占空比依然是马鞍波。当我用手去给电机施加阻力时&#xff0c;PID要维持目标转速&am…

Qt 5.14.2 学习记录 —— 이십일 Qt网络和音频

文章目录 1、UDP带有界面的Udp服务器&#xff08;回显服务器&#xff09; 2、TCP回显服务器 3、HTTP客户端4、音频 和Linux的网络一样&#xff0c;Qt封装了Linux的网络API&#xff0c;即Socket API。网络编程是在应用层写&#xff0c;需要传输层支持&#xff0c;传输层有UDP和T…