*胡闹厨房*
前期准备
详细教程
一、创建项目
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