胡闹厨房练习(一)

参考教程
参考教程

学习视频

目录

准备工作

一、创建项目

二、处理预置文件和设置

三、编辑器布局

四、Visual Studio设置

 五、导入资源

六、设置源素材

七、视觉效果(后期处理)

角色控制器

一、角色

二、制作动画

三、动画控制

四、运动脚本

五、Cinemachine

角色移动的重新设置

一、重构代码

二、输入系统

三、设置输入事件

四、碰撞检测

厨房柜台

一、添加和设置柜台

二、角色与柜台产生交互

三、编辑器中的交互事件

选中柜台

一、被选中柜台的视觉效果

二、角色发送事件,以显示对应柜台的视觉效果

在柜台上生成物品

一、添加食材

二、生成物品

三、食材管理

 四、获取柜台上放置的物体信息

五、物品更换柜台

六、接口

七、拿起和放下物品

柜台

一、创建不同的柜台

二、

三、增加柜台

委托事件

一、委托事件相关知识

(一)委托事件的实例

(二)委托事件的组成:

(三)委托事件的应用

二、设置委托事件

重构代码

一、初步调整

二、重新整理

柜台动画

一、打开柜子,拿出物品

二、放下物品

三、增加检查

柜台变体

一、制作柜台

二、新的食物

三、放置其他食材的柜台

重构场景

一、食材柜台与空柜台

二、食材处理柜台

三、切片交互

四、代码整理

五、切割对象

六、切菜进度条

七、切菜进度条朝向摄像机


准备工作

一、创建项目

二、处理预置文件和设置

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

2、Edit-Project Setting-Graphics,默认设置

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

4、Assets-Settings

(1) 删除选中文件

 (2) 误删后的操作

点击“+”号,Rendering中添加(或到回收站中找回)

三、编辑器布局

1、Game 设置

2、Scene设置

3、整体布局

四、Visual Studio设置

安装扩展——Viasfora

 五、导入资源

拖入Project,在弹出框中点击Inport

六、设置源素材

1、重命名场景为GameScene

2、删除SampleSceneProfile

3、选中Global Volume,在它的Inspector面板找到Profile,点击New

 

七、视觉效果(后期处理)

1、添加地板

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

(2) 设置材质

2、添加角色

(1) 向场景添加PlayerVisual、ClearCounter_Visual、CuttingCounter_Visual、Tomato_Visual等

(2) 添加灶台,将灶台的子物体StoveOnVisual设为可见

3、定位摄像机

(1) 快捷方法:选中Main Camera,Ctrl+Shift+F

(2) 利用选项卡:

(3) 直接拖动摄像机,调整位置,同时调整 Projection下的Field of View为20

4、视觉效果

(1) 将视图切换到Game

(2) 选中Main Camera,确保Rendering下的Post Processing为勾选状态

(3) 确保Assets-Setting下包含URP-HighFidelity,且Quality下的HDR为勾选状态,

MSAA改为8x(抗锯齿)

(4) 确保URP-HighFidelity-Renderer的Post-processing下Enable为勾选状态

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

(6) 选择Global Volume,点击Add Override,添加Tonemapping(颜色校正),Mode选择Neutrual

(7) 选择Global Volume,点击Add Override,添加并设置Color Adjustments

(8) 选择Global Volume,点击Add Override,添加并设置Bloom(泛光)

(9) 选择Global Volume,点击Add Override,添加并设置Vignette(晕影)

(10) 灯光(默认)

5、更改视觉效果:

(1) 选择Global Volume,点击Clone按钮,可见Project文件夹中增加了新的文件

(2) 选择克隆文件进行修改。

(3) 选择原文件可以恢复,删除克隆的文件,删除添加的除Floor外的物品

角色控制器

一、角色

1、创建角色

(1) Create Empty,命名为Player,Reset它的Transform(优点:无需在脚本中加入视觉的逻辑)

(2) 以Player为父物体,3D Object-Capsule,设置Transform

(3) 将PlayerVisual作为Player的子物体拖放到HIerarchy面板上,重置Transform

(4) 删除Capsule

2、角色移动

(1) 新建Scripts文件夹

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[SerializeField] private float moveSpeed = 7f;private void Update(){Vector2 inputVector = new Vector2(0, 0);if (Input.GetKey(KeyCode.W)){inputVector.y = +1;}if (Input.GetKey(KeyCode.S)){inputVector.y -= 1;}if (Input.GetKey(KeyCode.A)){inputVector.x -= 1;}if (Input.GetKey(KeyCode.D)){inputVector.x += 1;}inputVector = inputVector.normalized;Vector3 moveDir = new Vector3(inputVector.x, 0f, inputVector.y);transform.position += moveDir * moveSpeed * Time.deltaTime;Debug.Log(Time.deltaTime);}
}

3、‌着色器: Shader Graph 帮助开发者快速实现游戏内物体的着色效果

4、增加旋转

inputVector = inputVector.normalized;Vector3 moveDir = new Vector3(inputVector.x, 0f, inputVector.y);
transform.position += moveDir * moveSpeed * Time.deltaTime;float rotationSpeed = 10f;
transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime*rotationSpeed);
二、制作动画

1、学习设置Animator组件

(1) 复制Player,删除其子物体PlayerVisual的Animator组件,添加新的Animator组件

(2) 选中Setting文件夹,点击左上角 “+” 号,添加Animator Controller,命名

(3) 给 PlayerVisual的Animator组件中的Controller添加控制器

2、制作 Idle 状态的动画

(1) 双击上图中的 MyPlayerAnimator,打开动画控制器

(2) 打开Animation窗口:Window-Animation-Animation

(3) 点击上图中右侧的Create,在弹出窗口重命名为Idle,可见在Animator窗口出现Idle

Project窗口可见Idle文件,确保Loop Time 为勾选状态(循环) 

(4) 选中HIerarchy面板上的PlayerVisual,点击红色的录制按钮,使时间轴起始线在第 0 帧的位置

(5) 选中PlayerVisual的子物体Head,设置时间轴的第一个记录点

可见起始线上增加两个菱形点,改变的Position呈暗红色 

(5) 拖动时间线到第30帧(0.5秒)

(6) 复制第 0 帧的动画,粘贴到第60帧

(7) 选中PlayerVisual的子物体Body,选中第0帧,移动body后恢复,以记录body的关键帧

(8) 再次点击红色按钮,停止录制

3、制作Walk动画

(1) 复制 Idle,重命名为 Walk

(2) 将Project-Setting文件夹下的Walk拖放到动画控制器中

(3) 选中PlayerVisual的子物体 body,选择Animation面板中的 Walk,设置Walk的Position(y)

第0、0.1帧 0.65

第0.03、0.08帧 0.7685

第0.06、0.1帧 0.887

三、动画控制

1、Idle 与 Walk 动画的转换

(1) 添加 Bool 类参数,IsWalking

(2) 连接Idle与Walk,取消勾选Has Exit Time,设置IsWalking为true,另一个连接为 false

四、运动脚本

1、编辑Player.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[SerializeField] private float moveSpeed = 7f;//角色动画private bool isWalking;private void Update(){Vector2 inputVector = new Vector2(0, 0);if (Input.GetKey(KeyCode.W)){inputVector.y = +1;}if (Input.GetKey(KeyCode.S)){inputVector.y -= 1;}if (Input.GetKey(KeyCode.A)){inputVector.x -= 1;}if (Input.GetKey(KeyCode.D)){inputVector.x += 1;}inputVector = inputVector.normalized;Vector3 moveDir = new Vector3(inputVector.x, 0f, inputVector.y);transform.position += moveDir * moveSpeed * Time.deltaTime;//角色动画isWalking = moveDir != Vector3.zero;float rotationSpeed = 10f;transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);}//角色动画public bool IsWalking(){return isWalking;}
}

2、角色移动

(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) 赋值

五、Cinemachine

1、安装包

2、创建虚拟相机

3、设置虚拟相机的Position和Lens Vertical FOV

4、设置相机的Noise——场景动态效果

(1) 设置Noise

(2) 将Noise设置为None

5、相机跟随(本项目不用)

角色移动的重新设置

一、重构代码

1、Create Empty,命名为GameInput,Reset它的Transform。添加GameInput.cs组件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class GameInput : MonoBehaviour
{public Vector2 GetMovementVectorNormalized(){Vector2 inputVector = new Vector2(0, 0);if (Input.GetKey(KeyCode.W)){inputVector.y += 1;}if (Input.GetKey(KeyCode.S)){inputVector.y -= 1;}if (Input.GetKey(KeyCode.A)) {inputVector.x -= 1;}if (Input.GetKey(KeyCode.D)){inputVector.x += 1;}inputVector = inputVector.normalized;return inputVector;}
}

2、编辑Player.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[SerializeField] private float moveSpeed = 7f;//输入[SerializeField] private GameInput gameInput;private bool isWalking;private void Update(){//输入Vector2 inputVector = gameInput.GetMovementVectorNormalized();Vector3 moveDir = new Vector3(inputVector.x, 0f, inputVector.y);transform.position += moveDir * moveSpeed * Time.deltaTime;isWalking = moveDir != Vector3.zero;float rotationSpeed = 10f;transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);}public bool IsWalking(){return isWalking;}
}

3、赋值

二、输入系统

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

 2、设置输入系统

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

(2) 弹出框选择Apply,Unity编辑器自动重启

三、设置输入事件

1、在Settings文件夹下右键->Create->Input Actions,命名为PlayerInputActions,双击打开

2、添加输入事件

(1) 点击左上角 “+” 号命名为Player,将New action重命名为Move

(2) 更改Action Type和Control Type的值

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

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

(5) 点击 Save Asset

3、关联角色和输入事件

(1) 方法一:使用内置组件:为Hierarchy面板上的Player添加并设置Player Input组件

(2) 删除步骤(1) 中添加的组件

(3) 方法2:自动生成脚本:勾选Generate C# Class,点击Apply——本练习使用的方法

4、游戏对象被唤醒时启用玩家输入

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();}public Vector2 GetMovementVectorNormalized(){Vector2 inputVector = playerInputActions.Player.Move.ReadValue<Vector2>();inputVector = inputVector.normalized;Debug.Log(inputVector);return inputVector;}
}

归一化的操作(inputVector = inputVector.normalized;)也可在Input文件中添加processor

 

 5、添加键盘上的箭头控制

6、添加游戏手柄控制

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

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

(3) 点击Path右侧可选框,Joystick-listen,摇动摇杆,回车(或者在弹出框选择)

四、碰撞检测

1、添加障碍物:3D Object-Cube,确保:

(1) Cube底部接触到地板且含有Collider组件(默认)

(2) Player的Position.y=0

(3) Floor包含Mesh Collider组件且在Y轴上没有旋转(默认)

2、增加射线检测:编辑Player.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;private bool isWalking;private void Update(){Vector2 inputVector = gameInput.GetMovementVectorNormalized();Vector3 moveDir = new Vector3(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 = !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 = !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);}public bool IsWalking(){return isWalking;}
}

厨房柜台

一、添加和设置柜台

1、准备

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

(2) Create Empty ,命名为ClearCounter,设置Transform,添加并设置 Box Collider组件

2、添加柜台,重置Transform

3、制作柜台预制体放入Counters文件夹

二、角色与柜台产生交互

1、为预制体 ClearCounter 添加ClearCounter.cs组件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{public void Interact(){Debug.Log("Interact");}
}

2、为预制体 ClearCounter 添加图层 Counters

3、角色与柜台的碰撞事件

(1) 编辑Player.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;private bool isWalking;private Vector3 lastInteractDir;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 ClearCounter clearCounter)){clearCounter.Interact();}}else{Debug.Log("-");}}private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNormalized();Vector3 moveDir = new Vector3(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 = !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 = !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}isWalking = moveDir != Vector3.zero;float rotationSpeed = 10f;transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);}
}

后续更改的代码

    private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();Vector3 moveDir = new Vector3(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 != 0 && !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 != 0 && !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}isWalking = moveDir != Vector3.zero;float rotationSpeed = 10f;transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);}

(2) 赋值

三、编辑器中的交互事件

1、添加交互(Interact)事件

(1) 打开Setting文件夹中的 PlayerInputActions,(点击Action后面的加号)

(2) 选中No Binding,设置Path 为字母 E,点击 Save Asset

2、编辑GameImput.cs

(1) 点击弹出框底部的闪电图标

(2) 选择performed

(3) 输入+=后,按Tab

using System;//命名空间
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;public class GameInput : MonoBehaviour
{public event EventHandler OnInteractAction;private PlayerInputActions playerInputActions;private void Awake(){playerInputActions = new PlayerInputActions();playerInputActions.Player.Enable();playerInputActions.Player.Interact.performed += Interact_performed;}private void Interact_performed(UnityEngine.InputSystem.InputAction.CallbackContext obj){OnInteractAction?.Invoke(this, EventArgs.Empty);}public Vector2 GetMovementVectorNormalized(){Vector2 inputVector = playerInputActions.Player.Move.ReadValue<Vector2>();inputVector = inputVector.normalized;Debug.Log(inputVector);return inputVector;}
}
OnInteractAction?.Invoke(this,EventArgs.Empty);

等同于

if (OnInteractAction != null)
{OnInteractAction.Invoke(this, EventArgs.Empty);
}

3、编辑Player.cs

(1) 点击弹出框底部的闪电图标

(2) 选择OnInteractAction, 输入+=后,按Tab,按回车

(3) 点击字母E时,角色与柜台发生互动

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;private bool isWalking;private Vector3 lastInteractDir;private void Start(){gameInput.OnInteractAction += GameInput_OnInteractAction;}private void GameInput_OnInteractAction(object sender, System.EventArgs e){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,moveDir, out RaycastHit raycastHit,interactDistance,countersLayerMask)){if(raycastHit.transform.TryGetComponent(out ClearCounter clearCounter)){clearCounter.Interact();}}}private void Update(){HandleMovement();HandleInteractions();}public bool IsWalking(){return isWalking;}//交互public 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 ClearCounter clearCounter)){clearCounter.Interact();}}}//移动private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNormalized();Vector3 moveDir = new Vector3(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 = !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 = !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);}
}

选中柜台

一、被选中柜台的视觉效果

1、编辑 ClearCounter预制体

(1) 复制ClearCounter_Visual,重命名为Selected,设置它的Scale为1.01,1.01,1.01

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

禁用(隐藏)KitchenCounter

2、为Selected 添加 SelectedCounterVisual.cs

3、编辑Player.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;private bool isWalking;private Vector3 lastInteractDir;private ClearCounter selectedCounter;private void Start(){gameInput.OnInteractAction += GameInput_OnInteractAction;}private void GameInput_OnInteractAction(object sender, System.EventArgs e){if (selectedCounter != null){selectedCounter.Interact();}}private void Update(){HandleMovement();HandleInteractions();}public bool IsWalking(){return isWalking;}private void HandleInteractions(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();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 ClearCounter clearCounter)){if (clearCounter != selectedCounter){selectedCounter = clearCounter;}}else{selectedCounter = null;}}else{selectedCounter = null;}Debug.Log(selectedCounter);}private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();Vector3 moveDir = new Vector3(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 = !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 = !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}isWalking = moveDir != Vector3.zero;float rotationSpeed = 10f;transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);}
}
二、角色发送事件,以显示对应柜台的视觉效果

1、在Player.cs中,添加一个EventHandler委托,并使用EventArgs传递选中的柜台信息,在Update()中触发事件

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{public event EventHandler<OnselectedCounterChangedEventArgs> OnSelectedCounterChanged;public class OnselectedCounterChangedEventArgs : EventArgs{public ClearCounter selectedCounter;}[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;private bool isWalking;private Vector3 lastInteractDir;private ClearCounter selectedCounter;private void Start(){gameInput.OnInteractAction += GameInput_OnInteractAction;}private void GameInput_OnInteractAction(object sender, System.EventArgs e){if (selectedCounter != null){selectedCounter.Interact();}}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 ClearCounter clearCounter)){if (clearCounter != selectedCounter){SetSelectedCounter(clearCounter);}}else{SetSelectedCounter(null);}}else{SetSelectedCounter(null);}}private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNormalized();Vector3 moveDir = new Vector3(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 = !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 = !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}isWalking = moveDir != Vector3.zero;float rotationSpeed = 10f;transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);}private void SetSelectedCounter(ClearCounter selectedCounter){this.selectedCounter = selectedCounter;OnSelectedCounterChanged?.Invoke(this, new OnselectedCounterChangedEventArgs{selectedCounter = selectedCounter});}
}

2、编辑Player.cs,创建一个单例

public static Player Instance { get; private set; }
private void Awake()
{if (Instance != null) { Debug.LogError("There is more than one Player instance"); }Instance = this;
}

3、编辑SelectedCounterVisual.cs

(1) +=后面按Tab

(2) Instance_OnSelectedCounterChanged重命名为Player_OnSelectedCounterChanged

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class SelectedCounterVisual : MonoBehaviour
{[SerializeField] private ClearCounter clearCounter;[SerializeField] private GameObject visualGameObject;private void Start(){Player.Instance.OnSelectedCounterChanged += Player_OnSelectedCounterChanged;}private void Player_OnSelectedCounterChanged(object sender, Player.OnselectedCounterChangedEventArgs e){if(e.selectedCounter == clearCounter){Show();}else{Hide();}}private void Show(){visualGameObject.SetActive(true);}private void Hide(){visualGameObject.SetActive(false);}
}

4、赋值

(1) 打开 ClearCounter预制体

(2) 选择Selected,赋值(注意Selected为勾选状态)

在柜台上生成物品

一、添加食材

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

2、制作番茄预制体

(1) Create Empty,命名为Tomato,设置Transform,添加子物体Tomato_Visual

(2) 将Tomato制成预制体,放入KitchenObjects文件夹。之后,删除Hierarchy面板上的Tomato

二、生成物品

1、定位番茄生成的位置

(1) 打开 ClearCounter预制体

(2) Create Empty,命名为 CounterTopPoint

2、生成番茄

(1) 编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private Transform tomatoPrefab;[SerializeField] private Transform counterToPoint;public void Interact(){Debug.Log("Interact");Transform tomatoTransform = Instantiate(tomatoPrefab,counterToPoint);tomatoPrefab.localPosition = Vector3.zero;}
}

(2) 赋值

(3) 测试结果:接近柜台时,柜台变色,点击E时,生成番茄

3、制作CheeseBlock预制体

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

(2) 打开CheeseBlock预制体

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

三、食材管理

1、新建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;
}

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

3、在KitchenObjectSO文件夹下,新建 Kitchen Object SO 文件,重命名为Tomato

4、放置番茄

(1) 编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;public void Interact(){Debug.Log("Interact");Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.localPosition = Vector3.zero;}
}

(2) 赋值

5、在KitchenObjectSO文件夹下,新建 Kitchen Object SO 文件,命名为CheeseBlock

6、放置厨房物品

(1) 为预制体 Tomato 和 CheeseBlock 添加 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) 为两个预制体赋值

7、编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;public void Interact(){Debug.Log("Interact");Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.localPosition = Vector3.zero;Debug.Log(kitchObjectTransform.GetComponent<KitchenObject>().GetKitchenObjectSO().objectName);}
}
 四、获取柜台上放置的物体信息

1、编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;private KitchenObject kitchenObject;public void Interact(){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.localPosition = Vector3.zero;kitchenObject = kitchObjectTransform.GetComponent<KitchenObject>();}}
}

2、编辑KitchenObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class KitchenObject : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;private ClearCounter clearCounter;public KitchenObjectSO GetKitchenObjectSO(){return kitchenObjectSO;}public void SetClearCounter(ClearCounter clearCounter){this.clearCounter = clearCounter;}public ClearCounter GetClearCounter(){return clearCounter;}
}

 3、编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;private KitchenObject kitchenObject;public void Interact(){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.localPosition = Vector3.zero;kitchenObject = kitchObjectTransform.GetComponent<KitchenObject>();kitchenObject.SetClearCounter(this);}else{Debug.Log(kitchenObject.GetClearCounter());}}
}

4、

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;[SerializeField] private ClearCounter secondClearCounter;[SerializeField] private bool testing;private KitchenObject kitchenObject;private void Update(){if (testing && Input.GetKeyDown(KeyCode.T)){if(kitchenObject != null){kitchenObject.SetClearCounter(secondClearCounter);Debug.Log(kitchenObject.GetClearCounter());}}}public void Interact(){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.localPosition = Vector3.zero;kitchenObject = kitchObjectTransform.GetComponent<KitchenObject>();kitchenObject.SetClearCounter(this);}else{Debug.Log(kitchenObject.GetClearCounter());}}
}

 选择Hierarchy面板上的ClearCounter,赋值

五、物品更换柜台

1、

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;[SerializeField] private ClearCounter secondClearCounter;[SerializeField] private bool testing;private KitchenObject kitchenObject;private void Update(){if (testing && Input.GetKeyDown(KeyCode.T)){if(kitchenObject != null){kitchenObject.SetClearCounter(secondClearCounter);Debug.Log(kitchenObject.GetClearCounter());}}}public void Interact(){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.localPosition = Vector3.zero;kitchenObject = kitchObjectTransform.GetComponent<KitchenObject>();kitchenObject.SetClearCounter(this);}else{Debug.Log(kitchenObject.GetClearCounter());}}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}
}

2、

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class KitchenObject : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;private ClearCounter clearCounter;public KitchenObjectSO GetKitchenObjectSO(){return kitchenObjectSO;}public void SetClearCounter(ClearCounter clearCounter){this.clearCounter = clearCounter;transform.parent = clearCounter.GetKitchenObjectFollowTransform();transform.localPosition = Vector3.zero;}public ClearCounter GetClearCounter(){return clearCounter;}
}

3、

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;[SerializeField] private ClearCounter secondClearCounter;[SerializeField] private bool testing;private KitchenObject kitchenObject;private void Update(){if (testing && Input.GetKeyDown(KeyCode.T)){if (kitchenObject != null){kitchenObject.SetClearCounter(secondClearCounter);Debug.Log(kitchenObject.GetClearCounter());}}}public void Interact(){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.localPosition = Vector3.zero;kitchenObject = kitchObjectTransform.GetComponent<KitchenObject>();kitchenObject.SetClearCounter(this);}else{Debug.Log(kitchenObject.GetClearCounter());}}//获取当前物体跟随厨房柜台的变换信息public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}//设置厨柜上的物品public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}//获取厨柜上的物品public KitchenObject GetKitchenObject(){return kitchenObject;}//清除厨柜上的物品public void ClearKitchenObject(){kitchenObject = null;}/// <summary>/// 检查厨房柜台上是否有物品。/// </summary>/// <returns>如果厨房柜台上有物品,则为 true;否则为 false。</returns>public bool HasKitchenObject(){return kitchenObject != null;}
}

4、

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class KitchenObject : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;private ClearCounter clearCounter;public KitchenObjectSO GetKitchenObjectSO(){return kitchenObjectSO;}public void SetClearCounter(ClearCounter clearCounter){if(this.clearCounter != null){this.clearCounter.ClearKitchenObject();}this.clearCounter = clearCounter;if(clearCounter.HasKitchenObject()){Debug.LogError("ClearCounter already has a KitchenObject!");}clearCounter.SetKitchenObject(this);transform.parent = clearCounter.GetKitchenObjectFollowTransform();transform.localPosition = Vector3.zero;}public ClearCounter GetClearCounter(){return clearCounter;}
}

5、

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;[SerializeField] private ClearCounter secondClearCounter;[SerializeField] private bool testing;private KitchenObject kitchenObject;private void Update(){if (testing && Input.GetKeyDown(KeyCode.T)){if (kitchenObject != null){kitchenObject.SetClearCounter(secondClearCounter);}}}public void Interact(){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetClearCounter(this);}else{Debug.Log(kitchenObject.GetClearCounter());}}//获取当前物体跟随厨房柜台的变换信息public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}//设置厨柜上的物品public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}//获取厨柜上的物品public KitchenObject GetKitchenObject(){return kitchenObject;}//清除厨柜上的物品public void ClearKitchenObject(){kitchenObject = null;}/// <summary>/// 检查厨房柜台上是否有物品。/// </summary>/// <returns>如果厨房柜台上有物品,则为 true;否则为 false。</returns>public bool HasKitchenObject(){return kitchenObject != null;}
}
六、接口

1、实例

// 定义一个名为 IKitchenObjectParent 的接口
public interface IKitchenObjectParent
{Transform GetKitchenObjectFollowTransform();
}
// 实现接口的类
public class Player : MonoBehaviour, IKitchenObjectParent
{public Transform counterToPoint;// GetKitchenObjectFollowTransform 方法的具体实现public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}
}
// 实现接口的类
public class BaseCounter : MonoBehaviour, IKitchenObjectParent
{public Transform stove;// GetKitchenObjectFollowTransform 方法的具体实现public Transform GetKitchenObjectFollowTransform(){return stove;}
}

 在上面的示例中,首先定义了一个名为 IKitchenObjectParent 的接口,它包含了一个名为 GetKitchenObjectFollowTransform 的方法,该方法返回一个 Transform 对象。

接下来,PlayerBaseCounter 类都实现了 IKitchenObjectParent 接口,并分别提供了它们自己的 GetKitchenObjectFollowTransform 方法的具体实现。在 Player 类中,counterToPoint 是一个 Transform 类型的属性,它被用作返回值。而在 BaseCounter 类中,stove 是另一个 Transform 类型的属性,它被用作返回值。

通过这种方式,在其他地方使用 IKitchenObjectParent 接口的实例时,可以使用 PlayerBaseCounter 类的实例,并调用 GetKitchenObjectFollowTransform 方法来获取不同的厨房对象的跟随位置(根据具体的实现类)。这样可以实现代码的灵活性和可扩展性。

2、接口脚本的一些规则

  1. 使用 interface 关键字定义接口。例如:public interface IKitchenObjectParent { }

  2. 接口的命名应尽量清晰明了,表达出接口的用途和功能。

  3. 方法的声明只包含方法名、参数列表和返回类型,不包含方法的具体实现。

  4. 方法的返回类型可以是任意合法的数据类型,也可以是其他接口或类的类型。

  5. 接口中的方法通常不应包含访问修饰符,因为接口中定义的方法默认为公共(public)。

  6. 接口中可以包含属性、事件和索引器等成员,但通常应尽量保持接口的简洁和清晰。接口的目的是定义一组行为,而不是状态。

  7. 接口可以继承其他接口,使用冒号(:)来表示接口之间的继承关系。例如:public interface IChildInterface : IParentInterface { }

  8. 类可以实现一个或多个接口,使用冒号(:)来表示实现的接口。例如:public class MyClass : IInterface1, IInterface2 { }

  9. 实现接口的类必须提供接口中的所有成员的具体实现,包括方法、属性和事件等。

  10. 实现接口的类可以选择性地添加其他成员,但在使用接口的实例时,只能访问接口中定义的成员。

3、实现接口的类:在Unity中实现接口的类遵循以下规则:

  1. 使用关键字 : MonoBehaviour: ScriptableObject 标记类继承自 MonoBehaviourScriptableObject,这是Unity中常用的基类。例如:
public class MyClass : MonoBehaviour, MyInterface {// 类的实现代码
}

public class MyClass : ScriptableObject, MyInterface {// 类的实现代码
}
  1. 实现接口的类必须实现接口中定义的所有方法,包括事件的处理方法。

  2. 可以选择性地覆盖接口中定义的默认方法。

  3. 可以实现多个接口,只需使用逗号将接口名称分隔开。例如:

public class MyClass : MonoBehaviour, IInterface1, IInterface2 {// 类的实现代码
}
七、拿起和放下物品

1、定义厨房物品的父对象(厨柜)的行为:新建 IkitchenObjectParent.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public interface IkitchenObjectParent 
{public Transform GetKitchenObjectFollowTransform();public void SetKitchenObject(KitchenObject kitchenObject);public KitchenObject GetKitchenObject();public void ClearKitchenObject();public bool HasKitchenObject();
}

2、编辑KitchenObject.cs

(1) 更改类型为IkitchenObjectParent,重命名为kitchenObjectParent

(2) 重命名为SetKitchenObjectParent

(3) 重命名为kitchenObjectParent

(4) 重命名为GetKitchenObjectParent

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class KitchenObject : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;private IkitchenObjectParent kitchenObjectParent;public KitchenObjectSO GetKitchenObjectSO(){return kitchenObjectSO;}public void SetKitchenObjectParent(IkitchenObjectParent kitchenObjectParent){if(this.kitchenObjectParent != null){this.kitchenObjectParent.ClearKitchenObject();}this.kitchenObjectParent = kitchenObjectParent;if(kitchenObjectParent.HasKitchenObject()){Debug.LogError("IkitchenObjectParent already has a KitchenObject!");}kitchenObjectParent.SetKitchenObject(this);transform.parent = kitchenObjectParent.GetKitchenObjectFollowTransform();transform.localPosition = Vector3.zero;}public IkitchenObjectParent GetKitchenObjectParent(){return kitchenObjectParent;}
}

3、编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour,IkitchenObjectParent
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;[SerializeField] private ClearCounter secondClearCounter;[SerializeField] private bool testing;private KitchenObject kitchenObject;private void Update(){if (testing && Input.GetKeyDown(KeyCode.T)){if (kitchenObject != null){kitchenObject.SetKitchenObjectParent(secondClearCounter);}}}public void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{//kitchenObject.SetKitchenObjectParent(secondClearCounter)//Debug.Log(kitchenObject.GetKitchenObjectParent());}}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

5、编辑Player预制体

Create Empty,命名为KitchenObjectHoldPoint,做为Player的子物体;

6、编辑Player.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour, IkitchenObjectParent
{public static Player Instance { get; private set; }public event EventHandler<OnselectedCounterChangedEventArgs> OnSelectedCounterChanged;public class OnselectedCounterChangedEventArgs : EventArgs{public ClearCounter selectedCounter;}[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;[SerializeField] private Transform counterToPoint;private bool isWalking;private Vector3 lastInteractDir;private ClearCounter selectedCounter;private KitchenObject kitchenObject;private void Awake(){if (Instance != null){Debug.LogError("There is more than one Player instance");}Instance = this;}private void Start(){gameInput.OnInteractAction += GameInput_OnInteractAction;}private void GameInput_OnInteractAction(object sender, System.EventArgs e){if (selectedCounter != null){selectedCounter.Interact(this);}}private void Update(){HandleMovement();HandleInteractions();}public bool IsWalking(){return isWalking;}private void HandleInteractions(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();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 ClearCounter clearCounter)){if (clearCounter != selectedCounter){SetSelectedCounter(clearCounter);}}else{SetSelectedCounter(null);}}else{SetSelectedCounter(null);}}private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();Vector3 moveDir = new Vector3(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 = !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 = !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}isWalking = moveDir != Vector3.zero;float rotationSpeed = 10f;transform.forward = Vector3.Slerp(transform.forward, moveDir, Time.deltaTime * rotationSpeed);}private void SetSelectedCounter(ClearCounter selectedCounter){this.selectedCounter = selectedCounter;OnSelectedCounterChanged?.Invoke(this, new OnselectedCounterChangedEventArgs{selectedCounter = selectedCounter});}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

 重命名为kitchenObjectHoldPoint

赋值

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour,IkitchenObjectParent
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;[SerializeField] private ClearCounter secondClearCounter;[SerializeField] private bool testing;private KitchenObject kitchenObject;private void Update(){if (testing && Input.GetKeyDown(KeyCode.T)){if (kitchenObject != null){kitchenObject.SetKitchenObjectParent(secondClearCounter);}}}public void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{kitchenObject.SetKitchenObjectParent(player);}}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : MonoBehaviour,IkitchenObjectParent
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;private KitchenObject kitchenObject;public void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{kitchenObject.SetKitchenObjectParent(player);}}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

测试结果:点击E,放置,再次点击,拿起

柜台

一、创建不同的柜台

1、复制ClearCounter预制体,重命名为_BaseCounter,将原来ClearCounter预制体的重命名为ClearCounter_BACKUP

2、编辑_BaseCounter预制体

(1) 删除子物体 ClearCounter_Visual、Selected

(2) 移除_BaseCounter上的ClearCounter.cs组件

3、在_BaseCounter上右击,Create-Prefab Variant,重命名为ClearCounter

4、编辑新的ClearCounter预制体:

 (1) 给父物体ClearCounter添加ClearCounter.cs组件,赋值

(2) 复制ClearCounter_BACKUP的子物体ClearCounter_Visual和Selected(在Hierarchy上的位置)


(3) 选择Selected,赋值

(4) 删除预制体ClearCounter_BACKUP

(5) 删除Hierarchy面板上的ClearCounter 和ClearCounter (1)

二、

1、向场景添加ClearCounter。注意在Hierarchy上的位置和Transform

2、向场景添加第二个 ClearCounter

3、右击预制体 _BaseCounter,Create-Prefab Variant,重命名为ContainerCounter

4、编辑ContainerCounter预制体

(1) 添加 ContainerCounter_Visual

(2) 复制 ContainerCounter_Visual,重命名为Selected

(3) 设置Selected 的子物体的材质、隐藏这些子物体

(4) 设置Selected 的Scale,移除Animator

(5) 添加SelectedCounterVisual.cs 组件,并赋值

(6) 给ContainerCounter预制体添加ContainerCounter.cs组件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounter : MonoBehaviour,IkitchenObjectParent
{[SerializeField] private KitchenObjectSO kitchenObjectSO;[SerializeField] private Transform counterToPoint;private KitchenObject kitchenObject;public void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{kitchenObject.SetKitchenObjectParent(player);}}//获取当前物体跟随厨房柜台的变换信息public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

(7) 赋值

三、增加柜台

1、放置第三个柜台

2、放置第四个柜台

委托事件

一、委托事件相关知识

编写 C# 中的 Unity 项目时,需要实现自定义事件,可通过委托事件实现。

(一)委托事件的实例

例如(假设 OnButtonClick 是当前类 MyClass 中的一个事件,且在该类的某个方法中订阅该事件)

public class MyClass
{//委托类型public delegate void ButtonClickEventHandler(object sender, EventArgs e);//声明委托事件public event ButtonClickEventHandler OnButtonClick;public void SubscribeToEvent(){// 订阅事件this.OnButtonClick += HandleButtonClick;}// 事件处理程序public void HandleButtonClick(object sender, EventArgs e){// 点击时出现的行为Console.WriteLine("Button was clicked!");}// 触发事件的方法(当调用 ClickButton()方法时,检查是否有订阅,如果有,则触发事件,同时运行事件处理程序)public void ClickButton(){// 如果OnButtonClick不是null(即有订阅者),则触发事件OnButtonClick?.Invoke(this, EventArgs.Empty);}
}
(二)委托事件的组成:

1. 委托类型

(1) 含义:用于定义事件的签名,本身并不会直接起作用,它只是定义了一个委托类型

(2) 实例

类似于一个方法的原型,指定了事件处理程序应该具有的返回类型和参数类型,如:

(3) 解析

delegate :委托关键字,在C#中用于创建委托类型。

                  委托是一种类型安全的函数指针,可以用来引用一个或多个方法。

ButtonClickEventHandler: 委托的名称,可根据需要自定义。

(object sender, EventArgs e): 委托的参数列表(委托类型的签名)。

这个委托期望两个参数,一个是触发事件的对象,另一个是事件的参数。

sender参数通常是发送事件的对象本身,EventArgs是事件参数的基类。

object sender 表示事件的发送者,EventArgs e 表示事件参数。

(4) 作用

这段代码定义了一个委托类型ButtonClickEventHandler,用于定义按钮点击事件的处理方法。

委托类型可以用来创建具有相同签名的方法的实例,以便在事件被触发时调用。

(5) 注意:

使用一个现有的委托类型时,无需自己定义委托类型。例如(直接声明委托事件)

//声明一个名为 OnSelectedCounterChanged 的事件
public event EventHandler<OnSelectedCounterChangedEventArgs> OnSelectedCounterChanged;
//定义委托事件的参数(使用EventArgs传递选中的柜台信息)
public class OnSelectedCounterChangedEventArgs : EventArgs
{//注意,属性命名时首字母是大写(图片上写错了)public BaseCounter SelectedCounter;
}

EventHandler<OnSelectedCounterChangedEventArgs>是 .NET Framework 中的一个通用委托类型

EvenHandler<TEventArgs> 是一个预定义的委托类型,用于处理事件并传递自定义参数(TEventArgs)

它的签名类似于 void SomeMethod(object sender, TEventArgs e),其中 sender 表示事件的发送者,e 是传递给事件处理程序的参数。

(6) 使用

当需要定义一个按钮点击事件的处理方法时,可以使用此委托类型来声明一个委托实例,并将其绑定到按钮的点击事件上。当按钮被点击时,委托实例所引用的方法将被调用。

2. 事件声明:用于定义一个事件成员。

它与字段的声明类似,但以 `event` 关键字开头,后面跟着委托类型和事件的名称。如:

public event ButtonClickEventHandler OnButtonClick;

 再如:

// 定义委托事件的参数
public class OnselectedCounterChangedEventArgs : EventArgs
{//定义一个名为 selectedCounter 的属性,它存储了被选中柜子的相关信息public BaseCounter selectedCounter;
}
// 声明一个名为 OnSelectedCounterChanged 的事件
// 在触发OnSelectedCounterChanged事件时,将选中柜子作为参数传递给事件的订阅者(事件处理方法)。
// 事件的订阅者可以根据选中柜子的信息来进行相应的处理
public event EventHandler<OnselectedCounterChangedEventArgs> OnSelectedCounterChanged;

3. 事件处理程序

(1) 事件处理程序是一个方法,用于响应事件的发生。它必须与委托类型的签名匹配。例如:

public void HandleButtonClick(object sender, EventArgs e)
{// 点击时发生的行为
}

 (2) 实例

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

4. 订阅事件

(1) 将一个方法与事件关联起来,当事件触发时,关联的方法被调用。例如:

Button button = new Button();
button.OnButtonClick += HandleButtonClick; // 订阅事件

(2) 实例

// 订阅事件
gameInput.OnInteractAction += GameInput_OnInteractAction;
// 事件处理程序
private void GameInput_OnInteractAction(object sender, System.EventArgs e)
{if(selectedCounter!=null){selectedCounter.Interact(this);}
}

+= 运算符用于将事件处理程序(Event Handler)添加到事件中。

OnInteractAction事件被触发时,添加到这个事件的所有处理程序都会被执行

GameInput_OnInteractAction方法注册为gameInput对象的OnInteractAction事件的一个事件处理程序。

OnInteractAction事件被触发(比如玩家进行了一个交互动作),GameInput_OnInteractAction方法就会被执行,从而进行相应的处理。

​​​​​5. 触发事件

(1) 在事件发生时调用事件委托来通知所有的订阅者,让它们执行关联的方法。例如:

public void ClickButton()
{// 触发点击事件OnButtonClick?.Invoke(this, EventArgs.Empty);
}

(2) 在其它方法中,调用ClickButton()时,就会触发OnButtonClick事件,检查是否有订阅者,如果有订阅者,则执行事件处理程序

(三)委托事件的应用
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour
{//定义委托事件的参数(使用EventArgs传递选中的柜台信息)public class OnSelectedCounterChangedEventArgs : EventArgs{public BaseCounter SelectedCounter;}//声明一个名为 OnSelectedCounterChanged 的事件public event EventHandler<OnSelectedCounterChangedEventArgs> OnSelectedCounterChanged;private BaseCounter selectedCounter;private void HandleInteractions(){SetSelectedCouter(baseCounter);}//触发事件(当该方法被调用时,触发OnSelectedCounterChanged事件)private void SetSelectedCouter(BaseCounter selectedCounter){this.selectedCounter = selectedCounter;OnSelectedCounterChanged?.Invoke(this, new OnSelectedCounterChangedEventArgs{SelectedCounter = selectedCounter});}
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class SelectedCounterVisual : MonoBehaviour
{// 订阅事件private void Start(){Player.Instance.OnSelectedCounterChanged += Instance_OnSelectedCounterChanged;}// 事件处理程序private void Instance_OnSelectedCounterChanged(object sender, Player.OnSelectedCounterChangedEventArgs e){if(e.SelectedCounter == baseCounter){Show();}else{Hide();}}
}

执行顺序

当运行HandleInteractions()程序时,启动SetSelectedCouter(),向订阅者SelectedCounterVisual中的Start()传递两个信息,从而启动事件处理程序Instance_OnSelectedCounterChanged();

二、设置委托事件

1、新建BaseCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BaseCounter : MonoBehaviour
{public virtual void Interact(Player player){Debug.LogError("BaseCounter.Interact();");}
}

2、编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : BaseCounter,IkitchenObjectParent{……}//重写public override void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{kitchenObject.SetKitchenObjectParent(player);}}

3、编辑ContainerCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounter : BaseCounter,IkitchenObjectParent
{public override void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{kitchenObject.SetKitchenObjectParent(player);}}
}

4、编辑Player.cs,重命名为baseCounter

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour, IkitchenObjectParent
{public static Player Instance { get; private set; }public event EventHandler<OnselectedCounterChangedEventArgs> OnSelectedCounterChanged;public class OnselectedCounterChangedEventArgs : EventArgs{public BaseCounter selectedCounter;}[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;[SerializeField] private Transform counterToPoint;private bool isWalking;private Vector3 lastInteractDir;private BaseCounter selectedCounter;private KitchenObject kitchenObject;private void Awake(){if (Instance != null){Debug.LogError("There is more than one Player instance");}Instance = this;}private void Start(){gameInput.OnInteractAction += GameInput_OnInteractAction;}private void GameInput_OnInteractAction(object sender, System.EventArgs e){if (selectedCounter != null){selectedCounter.Interact(this);}}private void Update(){HandleMovement();HandleInteractions();}public bool IsWalking(){return isWalking;}private void HandleInteractions(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();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)){if (baseCounter != selectedCounter){SetSelectedCounter(baseCounter);}}else{SetSelectedCounter(null);}}else{SetSelectedCounter(null);}}private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();Vector3 moveDir = new Vector3(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 = !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 = !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}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 = selectedCounter});}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

5、编辑SelectedCounterVisual.cs

(1) 重命名为baseCounter

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);}}
}

(2) 编辑 ContainerCounter预制体(赋值)

(3) 删除Single door的 子物体 ObjectSprite

6、编辑ClearCounter预制体

重构代码

一、初步调整

1、给基类 BaseCounter.cs 增加并设置接口,注意声明

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BaseCounter : MonoBehaviour,IkitchenObjectParent
{[SerializeField] private Transform counterToPoint;//这个成员可以被该类本身、同一个包中的其他类以及该类的所有子类(无论子类是否在同一个包中)访问protected KitchenObject kitchenObject;public virtual void Interact(Player player){Debug.LogError("BaseCounter.Interact();");}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

2、给 BaseCounter.cs 的子类除去接口和相应的事件

(1) 编辑ContainerCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounter : BaseCounter
{[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{kitchenObject.SetKitchenObjectParent(player);}}
}

(2) 编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : BaseCounter
{[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){if (kitchenObject == null){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab, counterToPoint);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}else{kitchenObject.SetKitchenObjectParent(player);}}
}
二、重新整理

1、编辑 BaseCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BaseCounter : MonoBehaviour,IkitchenObjectParent
{[SerializeField] private Transform counterToPoint;//更改private KitchenObject kitchenObject;public virtual void Interact(Player player){Debug.LogError("BaseCounter.Interact();");}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

2、编辑ContainerCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounter : BaseCounter
{[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(player);}
}

3、编辑ClearCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : BaseCounter
{[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){}
}

柜台动画

一、打开柜子,拿出物品

1、编辑ContainerCounter预制体,选择ContainerCounter_Visual,双击Animator组件中的ContainerCounter动画,出现动画界面

 

2、给ContainerCounter预制体的子物体ContainerCounter_Visual添加ContainerCounterVisual.cs组件

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounterVisual : MonoBehaviour
{private Animator animator;private void Awake(){animator = GetComponent<Animator>();}
}

3、编辑ContainerCounter.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounter : BaseCounter
{public event EventHandler OnplayerGrabbedObject;//玩家抓取的对象上[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(player);OnplayerGrabbedObject?.Invoke(this,EventArgs.Empty);}
}

4、编辑 ContainerCounterVisual.cs并赋值

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounterVisual : MonoBehaviour
{private const string OPEN_CLOSE = "OpenClose";[SerializeField]private ContainerCounter containerCounter;private Animator animator;private void Awake(){animator = GetComponent<Animator>();}private void Start(){containerCounter.OnplayerGrabbedObject += ContainerCounter_OnplayerGrabbedObject;}private void ContainerCounter_OnplayerGrabbedObject(object sender, System.EventArgs e){animator.SetTrigger(OPEN_CLOSE);}
}

二、放下物品

1、编辑ClearCounter.cs:检查当前柜台是否是空柜台,如果是,检查玩家是否手持物品,如果是,物品放在空柜台上

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : BaseCounter
{[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingplayer.GetKitchenObject().SetKitchenObjectParent(this);}}else{//There is a KitchenObject here}}
}

2、测试:可见角色从储物柜拿出物品,放在空柜台上

3、将"KitchenObject"从柜台上取下,并将其交给玩家携带

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ClearCounter : BaseCounter
{[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingplayer.GetKitchenObject().SetKitchenObjectParent(this);}else{//Player not carrying anything}}else{//There is a KitchenObject hereif(player.HasKitchenObject()){//Player is carrying something}else{//Player is not carrying anythinbgGetKitchenObject().SetKitchenObjectParent(player);}}}
}
三、增加检查

1、编辑 ContainerCounterVisual.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounter : BaseCounter
{public event EventHandler OnplayerGrabbedObject;//玩家抓取的对象上[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){if (!player.HasKitchenObject()){Transform kitchObjectTransform = Instantiate(kitchenObjectSO.prefab);kitchObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(player);OnplayerGrabbedObject?.Invoke(this, EventArgs.Empty);}}
}

 2、测试结果:正常拿放

柜台变体

一、制作柜台

1、制作ContainerCounter预制体的变体,命名为 ContainerCounter_Tomato

2、编辑ContainerCounter_Tomato预制体:更改案面上图片

3、制作ContainerCounter预制体的变体,命名为 ContainerCounter_CheeseBlock

4、编辑ContainerCounter_CheeseBlock预制体

(1) 选择 ContainerCounter_CheeseBlock,更改ContainerCounter.cs组件中的赋值

(2) 保证案板面上图像为CheeseBlock

二、新的食物

1、创建kitchenObjectSO对象:Bread、Cabbage、MeatPattyUncooked

2、分配数据

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

(2) 编辑Bread预制体

(3) 删除Hierarchy面板上的Tomato_Visual,添加Bread_Visual

(4) 设置kitchenObjectSO对象Bread:三个都要改

(5) 同样的方法制作其他食材预制体和设置kitchenObjectSO对象

三、放置其他食材的柜台

1、制作ContainerCounter预制体的变体,命名为 ContainerCounter_Bread

2、编辑ContainerCounter_Bread预制体

(1) 选择 ContainerCounter_Bread,更改ContainerCounter.cs组件中的赋值

(2) 保证案板面上图像为Bread

3、同样的方法制作其他放置食材的柜台

重构场景

一、食材柜台与空柜台

Hierarchy和Scece布局如图

1、删除场景中的ContainerCounter

2、将ContainerCounter_CheeseBlock拖入场景

3、放置 ContainerCounter_Cabbage

4、删除ContainerCounter (1)

5、放置ContainerCounter_Tomato、ContainerCounter_MeatPattyUncooked

6、添加空柜台

(1) 复制Hierarchy面板上的ClearCounter (1) ,Position.x依次为-4.5、-6、3

(2) 其他Transform不变

(3) 复制ClearCounter (4),得到ClearCounter (5)

(4) 复制ClearCounter (5),得到ClearCounter (6),Position.x=-3,其他Transform不变

布局如下图

二、食材处理柜台

1、编辑Player.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour, IkitchenObjectParent
{public static Player Instance { get; private set; }public event EventHandler<OnselectedCounterChangedEventArgs> OnSelectedCounterChanged;public class OnselectedCounterChangedEventArgs : EventArgs{public BaseCounter selectedCounter;}[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;[SerializeField] private Transform counterToPoint;private bool isWalking;private Vector3 lastInteractDir;private BaseCounter selectedCounter;private KitchenObject kitchenObject;private void Awake(){if (Instance != null){Debug.LogError("There is more than one Player instance");}Instance = this;}private void Start(){gameInput.OnInteractAction += GameInput_OnInteractAction;}private void GameInput_OnInteractAction(object sender, System.EventArgs e){if (selectedCounter != null){selectedCounter.Interact(this);}}private void Update(){HandleMovement();HandleInteractions();}public bool IsWalking(){return isWalking;}private void HandleInteractions(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();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)){if (baseCounter != selectedCounter){SetSelectedCounter(baseCounter);}}else{SetSelectedCounter(null);}}else{SetSelectedCounter(null);}}private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();Vector3 moveDir = new Vector3(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 != 0 && !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 != 0 && !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}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 = selectedCounter});}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

2、Prefab文件夹下,右击_BaseCounter,创建预制体变形,命名为CuttingCounter

3、编辑CuttingCounter预制体

(1) 添加并复制CuttingCounter_Visual

(2) 将复制得到的CuttingCounter_Visual (1)重命名为Selected

(3) 移除Selected的Animator,设置Transfom、添加SelectedCounterVisual.cs组件,赋值

(4) 设置Selected的子物体的材质、隐藏这些子物体

4、给CuttingCounter预制体添加CuttingCounter.cs组件

(1) 代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounter : BaseCounter
{public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingplayer.GetKitchenObject().SetKitchenObjectParent(this);}else{//Player not carrying anything}}else{//There is a KitchenObject hereif (player.HasKitchenObject()){//Player is carrying something}else{//Player is not carrying anythinbgGetKitchenObject().SetKitchenObjectParent(player);}}}
}

(2) 赋值

(3) 选择Selected赋值

5、放置 CuttingCounter 柜台

6、组织Hierarchy面板

(1) Create Empty,命名为Counters,重置Transform

(2) 将所有橱柜作为 Counters的子物体

三、切片交互

1、添加新的交互事件 InteractAlternate

(1) 双击Setting文件夹下的PlayerInputActions

(2) 点击 Actions 右侧的加号,添加事件,命名为 InteractAlternate,设置点击F时交互,保存

2、编辑BaseCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BaseCounter : MonoBehaviour,IkitchenObjectParent
{[SerializeField] private Transform counterToPoint;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 counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

3、编辑GameInput.cs

(1) 注意按Tab键

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class GameInput : MonoBehaviour
{public event EventHandler OnInteractAction;public event EventHandler OnInteractAlternateAction;private PlayerInputActions playerInputActions;private void Awake(){playerInputActions = new PlayerInputActions();playerInputActions.Player.Enable();playerInputActions.Player.Interact.performed += Interact_performed;playerInputActions.Player.InteractAlternate.performed += InteractAlternate_performed;}private void InteractAlternate_performed(UnityEngine.InputSystem.InputAction.CallbackContext obj){OnInteractAlternateAction?.Invoke(this,EventArgs.Empty);}private void Interact_performed(UnityEngine.InputSystem.InputAction.CallbackContext obj){OnInteractAction?.Invoke(this, EventArgs.Empty);}public Vector2 GetMovementVectorNomalized(){Vector2 inputVector = playerInputActions.Player.Move.ReadValue<Vector2>();inputVector = inputVector.normalized;Debug.Log(inputVector);return inputVector;}
}

4、编辑Player.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Player : MonoBehaviour, IkitchenObjectParent
{public static Player Instance { get; private set; }public event EventHandler<OnselectedCounterChangedEventArgs> OnSelectedCounterChanged;public class OnselectedCounterChangedEventArgs : EventArgs{public BaseCounter selectedCounter;}[SerializeField] private float moveSpeed = 7f;[SerializeField] private GameInput gameInput;[SerializeField] private LayerMask countersLayerMask;[SerializeField] private Transform counterToPoint;private bool isWalking;private Vector3 lastInteractDir;private BaseCounter selectedCounter;private KitchenObject kitchenObject;private void Awake(){if (Instance != null){Debug.LogError("There is more than one Player instance");}Instance = this;}private void Start(){gameInput.OnInteractAction += GameInput_OnInteractAction;gameInput.OnInteractAlternateAction += GameInput_OnInteractAlternateAction;}private void GameInput_OnInteractAlternateAction(object sender, EventArgs e){if(selectedCounter != null){selectedCounter.InteractAlternate(this);}}private void GameInput_OnInteractAction(object sender, System.EventArgs e){if (selectedCounter != null){selectedCounter.Interact(this);}}private void Update(){HandleMovement();HandleInteractions();}public bool IsWalking(){return isWalking;}private void HandleInteractions(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();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)){if (baseCounter != selectedCounter){SetSelectedCounter(baseCounter);}}else{SetSelectedCounter(null);}}else{SetSelectedCounter(null);}}private void HandleMovement(){Vector2 inputVector = gameInput.GetMovementVectorNomalized();Vector3 moveDir = new Vector3(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 != 0 && !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 != 0 && !Physics.CapsuleCast(transform.position, transform.position + Vector3.up * playerHeight, playerRadius, moveDirZ, moveDistance);if (canMove){moveDir = moveDirZ;}else{//不能向任何方向移动}}}if (canMove){transform.position += moveDir * moveSpeed * Time.deltaTime;}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 = selectedCounter});}public Transform GetKitchenObjectFollowTransform(){return counterToPoint;}public void SetKitchenObject(KitchenObject kitchenObject){this.kitchenObject = kitchenObject;}public KitchenObject GetKitchenObject(){return kitchenObject;}public void ClearKitchenObject(){kitchenObject = null;}public bool HasKitchenObject(){return kitchenObject != null;}
}

 5、编辑CuttingCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounter : BaseCounter
{public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingplayer.GetKitchenObject().SetKitchenObjectParent(this);}else{//Player not carrying anything}}else{//There is a KitchenObject hereif (player.HasKitchenObject()){//Player is carrying something}else{//Player is not carrying anythinbgGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject()){//There is a KitchenObject here}}
}

6、编辑KitchenObject.cs

public void DestroySelf()
{kitchenObjectParent.ClearKitchenObject();Destroy(gameObject);
}

7、创建kitchenObjectSO对象

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

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

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

(4) 更改赋值

(5) 复制 Tomato 预制体,分别命名为 CheeseSlices、CabbageSlices

(6) 新建和设置 Kitchen Object SO 文件:CheeseSlices、CabbageSlices

8、设置kitchenObjectSO对象TomatoSlices

9、编辑CuttingCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounter : BaseCounter
{[SerializeField] private KitchenObjectSO cutKitchenObjectSO;public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingplayer.GetKitchenObject().SetKitchenObjectParent(this);}else{//Player not carrying anything}}else{//There is a KitchenObject hereif (player.HasKitchenObject()){//Player is carrying something}else{//Player is not carrying anythinbgGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject()){//There is a KitchenObject hereGetKitchenObject().DestroySelf();Transform kitchenObjectTransform = Instantiate(cutKitchenObjectSO.prefab);kitchenObjectTransform.GetComponent<KitchenObject>().SetKitchenObjectParent(this);}}
}

10、编辑CuttingCounter预制体,赋值

11、运行结果:按E拿放番茄,按F,番茄变成切片,再按E,拿到切片

四、代码整理

1、编辑KitchenObject.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class KitchenObject : MonoBehaviour
{[SerializeField] private KitchenObjectSO kitchenObjectSO;private IkitchenObjectParent kitchenObjectParent;public KitchenObjectSO GetKitchenObjectSO(){return kitchenObjectSO;}public void SetKitchenObjectParent(IkitchenObjectParent kitchenObjectParent){if (this.kitchenObjectParent != null){this.kitchenObjectParent.ClearKitchenObject();}this.kitchenObjectParent = kitchenObjectParent;if (kitchenObjectParent.HasKitchenObject()){Debug.LogError("IkitchenObjectParent already has a KitchenObject!");}kitchenObjectParent.SetKitchenObject(this);transform.parent = kitchenObjectParent.GetKitchenObjectFollowTransform();transform.localPosition = Vector3.zero;}public IkitchenObjectParent GetKitchenObjectParent(){return kitchenObjectParent;}public void DestroySelf(){kitchenObjectParent.ClearKitchenObject();Destroy(gameObject);}public static KitchenObject SpawnKitchenObject(KitchenObjectSO kitchenObjectSO,IkitchenObjectParent kitchenObjectParent){Transform kitchenObjectTransform = Instantiate(kitchenObjectSO.prefab);KitchenObject kitchenObject = kitchenObjectTransform.GetComponent<KitchenObject>();kitchenObject.SetKitchenObjectParent(kitchenObjectParent);return kitchenObject;}
}

2、编辑CuttingCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounter : BaseCounter
{[SerializeField] private KitchenObjectSO cutKitchenObjectSO;public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingplayer.GetKitchenObject().SetKitchenObjectParent(this);}else{//Player not carrying anything}}else{//There is a KitchenObject hereif (player.HasKitchenObject()){//Player is carrying something}else{//Player is not carrying anythinbgGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject()){//There is a KitchenObject hereGetKitchenObject().DestroySelf();KitchenObject.SpawnKitchenObject(cutKitchenObjectSO,this);}}
}

3、编辑ContainerCounter.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ContainerCounter : BaseCounter
{public event EventHandler OnplayerGrabbedObject;//玩家抓取的对象上[SerializeField] private KitchenObjectSO kitchenObjectSO;public override void Interact(Player player){if (!player.HasKitchenObject()){//Player is not carrying anythingKitchenObject.SpawnKitchenObject(kitchenObjectSO,player);OnplayerGrabbedObject?.Invoke(this, EventArgs.Empty);}}
}
五、切割对象

1、食材综合处理

(1) 新建CuttingRecipeSO.cs

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

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

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

(4) 分别创建和设置CuttingRecipeSO对象:Cabbage-CabbageSlices、CheeseBlock-CheeseSlices

2、食材与食材切片一一对应

(1) 编辑CuttingCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounter : BaseCounter
{[SerializeField] private CuttingRecipeSO[] cuttingRecipeSOArray;public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingplayer.GetKitchenObject().SetKitchenObjectParent(this);}else{//Player not carrying anything}}else{//There is a KitchenObject hereif (player.HasKitchenObject()){//Player is carrying something}else{//Player is not carrying anythinbgGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject()){//There is a KitchenObject hereKitchenObjectSO outputKitchenObjectSO = GetOutputForInput(GetKitchenObject().GetKitchenObjectSO());GetKitchenObject().DestroySelf();KitchenObject.SpawnKitchenObject(outputKitchenObjectSO, this);}}private KitchenObjectSO GetOutputForInput(KitchenObjectSO inputKitchenObjectSO){foreach(CuttingRecipeSO cuttingRecipeSO in cuttingRecipeSOArray){if(cuttingRecipeSO.input == inputKitchenObjectSO){return cuttingRecipeSO.output;}}return null;}
}

(2) 赋值

3、处理一些细节(可能会报错的地方:非切割食材和食材再次切割设置无应答)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounter : BaseCounter
{[SerializeField] private CuttingRecipeSO[] cuttingRecipeSOArray;public override void Interact(Player player){if (!HasKitchenObject()){//There is no KitchEnObject hereif (player.HasKitchenObject()){//Player is carrying somethingif (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO())){//Player carrying something that can be cutplayer.GetKitchenObject().SetKitchenObjectParent(this);}}else{//Player not carrying anything}}else{//There is a KitchenObject hereif (player.HasKitchenObject()){//Player is carrying something}else{//Player is not carrying anythinbgGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject() && HasRecipeWithInput(GetKitchenObject().GetKitchenObjectSO())){//There is a KitchenObject here And it can be cutKitchenObjectSO outputKitchenObjectSO = GetOutputForInput(GetKitchenObject().GetKitchenObjectSO());GetKitchenObject().DestroySelf();KitchenObject.SpawnKitchenObject(outputKitchenObjectSO, this);}}private bool HasRecipeWithInput(KitchenObjectSO inputKitchenObjectSO){foreach (CuttingRecipeSO cuttingRecipeSO in cuttingRecipeSOArray){if (cuttingRecipeSO.input == inputKitchenObjectSO){return true;}}return false;}private KitchenObjectSO GetOutputForInput(KitchenObjectSO inputKitchenObjectSO){foreach (CuttingRecipeSO cuttingRecipeSO in cuttingRecipeSOArray){if (cuttingRecipeSO.input == inputKitchenObjectSO){return cuttingRecipeSO.output;}}return null;}
}
六、切菜进度条

1、增加最大切割进度

(1) 编辑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) 设置CuttingRecipeSO对象:打开ScriptableObjects/CuttingRecipeSO文件夹,设置cuttingProgressMax(另外两个为3,3,)

2、按F 3或 5 次后,切片成功:编辑CuttingCounter.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounter : BaseCounter
{[SerializeField] private CuttingRecipeSO[] cuttingRecipeSOArray;// 切割进度private int cuttingProgress;public override void Interact(Player player){if (!HasKitchenObject()){// There is no KitchenObject hereif (player.HasKitchenObject()){// player is carrying somethingif (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO())){// Player carrying something that can be cutplayer.GetKitchenObject().SetKitchenObjectParent(this);// 初始切割进度cuttingProgress = 0;}}else{// Player not carrying anything}}else{// There is a KitchenObject hereif (player.HasKitchenObject()){// Player is carrying something}else{// Player is not carrying anythingGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject() && HasRecipeWithInput(GetKitchenObject().GetKitchenObjectSO())){// There is a KitchenObject her and it can be cut// 切割进度cuttingProgress++;CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(GetKitchenObject().GetKitchenObjectSO());if (cuttingProgress >= cuttingRecipeSO.cuttingProgressMax){KitchenObjectSO outputKitchObjectSO = GetOutputForInput(GetKitchenObject().GetKitchenObjectSO());GetKitchenObject().DestroySelf();KitchenObject.SpawnKitchenObject(outputKitchObjectSO, this);}}}// private bool HasRecipeWithInput(KitchenObjectSO inputKitchenObjectSO){CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(inputKitchenObjectSO);return cuttingRecipeSO != null;}// private KitchenObjectSO GetOutputForInput(KitchenObjectSO inputKitchenObjectSO){CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(inputKitchenObjectSO);if (cuttingRecipeSO != null){return cuttingRecipeSO.output;}else{return null;}}// 获取包含输入的切割配方private CuttingRecipeSO GetCuttingRecipeSOWithInput(KitchenObjectSO inputKitchenObjectSO){foreach (CuttingRecipeSO cuttingRecipeSO in cuttingRecipeSOArray){if (cuttingRecipeSO.input == inputKitchenObjectSO){return cuttingRecipeSO;}}return null;}
}

3、进度条滑块

(1) 创建画布

(2) 编辑CuttingCounter预制体,创建画布Canvas,重命名为ProgressBarUI

(3) 设置画布的Render Mode 为World Space

(4) Rect Transform中Pos.x,y,z=0,2.5,0,Width和Height=0,如上右图

(5) UI-Image,重命名为Bar,Rect Transform中Width和Height分别为1,0.2,颜色为FFCE00

(6) 设置进度条栏Bar的Sprite为White_1*1

(7) 设置Bar的Image组件中,Image Type为Filled,Fill Method改为Horizontal,Fill Amount 0.098

4、进度条背景

(1) 复制Bar,重命名为Background

(2) 更改Background的Image Type 为Simple,Image颜色为4D4D4D

(3) 互换Background和Bar在Hierarchy面板上的位置(使滑块显示出来)效果图如下右

(4) 给Bar添加Shadow组件,调节Effect Distance为0.1,-0.1——删除这个组件

(5) 给Background添加Shadow组件,调节Effect Distance,——删除这个组件

(6) 给Background添加outline组件调节Effect Distance为0.05,0.05,设置Effect Color的Alpha值为255

5、设置进度条

(1) 给ProgressBarUI 添加 ProgressBarUI.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class ProgressBarUI : MonoBehaviour
{[SerializeField] private CuttingCounter cuttingCounter;[SerializeField] private Image barImage;
}

(2) 赋值

6、添加委托事件

(1) 编辑 CuttingCounter.cs

using System;// 命名空间
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;public class CuttingCounter : BaseCounter
{// 委托事件public event EventHandler<OnprogressChangedEventArgs> OnProgressChanged;public class OnprogressChangedEventArgs : EventArgs{public float progressNormalized;}[SerializeField] private CuttingRecipeSO[] cuttingRecipeSOArray;private int cuttingProgress;public override void Interact(Player player){if (!HasKitchenObject()){// There is no KitchenObject hereif (player.HasKitchenObject()){// player is carrying somethingif (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO())){// Player carrying something that can be cutplayer.GetKitchenObject().SetKitchenObjectParent(this);cuttingProgress = 0;// CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(GetKitchenObject().GetKitchenObjectSO());OnProgressChanged?.Invoke(this, new OnprogressChangedEventArgs{progressNormalized = (float)cuttingProgress / cuttingRecipeSO.cuttingProgressMax});}}else{// Player not carrying anything}}else{// There is a KitchenObject hereif (player.HasKitchenObject()){// Player is carrying something}else{// Player is not carrying anythingGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject() && HasRecipeWithInput(GetKitchenObject().GetKitchenObjectSO())){// There is a KitchenObject her and it can be cutcuttingProgress++;CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(GetKitchenObject().GetKitchenObjectSO());// OnProgressChanged?.Invoke(this, new OnprogressChangedEventArgs{progressNormalized = (float)cuttingProgress / cuttingRecipeSO.cuttingProgressMax});if (cuttingProgress >= cuttingRecipeSO.cuttingProgressMax){KitchenObjectSO outputKitchObjectSO = GetOutputForInput(GetKitchenObject().GetKitchenObjectSO());GetKitchenObject().DestroySelf();KitchenObject.SpawnKitchenObject(outputKitchObjectSO, this);}}}// private bool HasRecipeWithInput(KitchenObjectSO inputKitchenObjectSO){CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(inputKitchenObjectSO);return cuttingRecipeSO != null;}// private KitchenObjectSO GetOutputForInput(KitchenObjectSO inputKitchenObjectSO){CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(inputKitchenObjectSO);if (cuttingRecipeSO != null){return cuttingRecipeSO.output;}else{return null;}}private CuttingRecipeSO GetCuttingRecipeSOWithInput(KitchenObjectSO inputKitchenObjectSO){foreach (CuttingRecipeSO cuttingRecipeSO in cuttingRecipeSOArray){if (cuttingRecipeSO.input == inputKitchenObjectSO){return cuttingRecipeSO;}}return null;}
}

7、编辑ProgressBarUI.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class ProgressBarUI : MonoBehaviour
{[SerializeField] private CuttingCounter cuttingCounter;[SerializeField] private Image barImage;private void Start(){cuttingCounter.OnProgressChanged += CuttingCounter_OnProgressChanged;barImage.fillAmount = 0f;Hide();}private void CuttingCounter_OnProgressChanged(object sender, CuttingCounter.OnprogressChangedEventArgs e){barImage.fillAmount = e.progressNormalized;if (e.progressNormalized == 0f || e.progressNormalized == 1f){Hide();}else{Show();}}private void Show(){gameObject.SetActive(true);}private void Hide(){gameObject.SetActive(false);}
}

8、增加委托事件:编辑CuttingCounter.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;public class CuttingCounter : BaseCounter
{public event EventHandler<OnprogressChangedEventArgs> OnProgressChanged;public class OnprogressChangedEventArgs : EventArgs{public float progressNormalized;}// 委托事件public event EventHandler Oncut;[SerializeField] private CuttingRecipeSO[] cuttingRecipeSOArray;private int cuttingProgress;public override void Interact(Player player){if (!HasKitchenObject()){// There is no KitchenObject hereif (player.HasKitchenObject()){// player is carrying somethingif (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO())){// Player carrying something that can be cutplayer.GetKitchenObject().SetKitchenObjectParent(this);cuttingProgress = 0;// CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(GetKitchenObject().GetKitchenObjectSO());OnProgressChanged?.Invoke(this, new OnprogressChangedEventArgs{progressNormalized = (float)cuttingProgress / cuttingRecipeSO.cuttingProgressMax});}}else{// Player not carrying anything}}else{// There is a KitchenObject hereif (player.HasKitchenObject()){// Player is carrying something}else{// Player is not carrying anythingGetKitchenObject().SetKitchenObjectParent(player);}}}public override void InteractAlternate(Player player){if (HasKitchenObject() && HasRecipeWithInput(GetKitchenObject().GetKitchenObjectSO())){// There is a KitchenObject her and it can be cutcuttingProgress++;// 委托事件Oncut?.Invoke(this,EventArgs.Empty);CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(GetKitchenObject().GetKitchenObjectSO());// OnProgressChanged?.Invoke(this, new OnprogressChangedEventArgs{progressNormalized = (float)cuttingProgress / cuttingRecipeSO.cuttingProgressMax});if (cuttingProgress >= cuttingRecipeSO.cuttingProgressMax){KitchenObjectSO outputKitchObjectSO = GetOutputForInput(GetKitchenObject().GetKitchenObjectSO());GetKitchenObject().DestroySelf();KitchenObject.SpawnKitchenObject(outputKitchObjectSO, this);}}}// private bool HasRecipeWithInput(KitchenObjectSO inputKitchenObjectSO){CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(inputKitchenObjectSO);return cuttingRecipeSO != null;}// private KitchenObjectSO GetOutputForInput(KitchenObjectSO inputKitchenObjectSO){CuttingRecipeSO cuttingRecipeSO = GetCuttingRecipeSOWithInput(inputKitchenObjectSO);if (cuttingRecipeSO != null){return cuttingRecipeSO.output;}else{return null;}}private CuttingRecipeSO GetCuttingRecipeSOWithInput(KitchenObjectSO inputKitchenObjectSO){foreach (CuttingRecipeSO cuttingRecipeSO in cuttingRecipeSOArray){if (cuttingRecipeSO.input == inputKitchenObjectSO){return cuttingRecipeSO;}}return null;}
}

9、设置切片动画

(1) 复制Project/Scripts 文件夹下的 ContainerCounterVisual.cs,重命名为CuttingCounterVisual

(2) 打开CuttingCounterVisual.cs,更改类名为CuttingCounterVisual

(3) 重命名OPEN_CLOSE为CUT,字符串改为“Cut”

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class CuttingCounterVisual : MonoBehaviour
{private const string CUT = "Cut";[SerializeField] private CuttingCounter cuttingCounter;private Animator animator;private void Awake(){animator = GetComponent<Animator>();}private void Start(){cuttingCounter.Oncut += CuttingCounter_Oncut;}private void CuttingCounter_Oncut(object sender, System.EventArgs e){animator.SetTrigger(CUT);}
}

(4) 编辑预制体CuttingCounter,给子物体CuttingCounter_Visual添加CuttingCounterVisual.cs组件

(5) 赋值

(6) 测试可见刀的动作

七、切菜进度条朝向摄像机

1、复制一个处理台

2、编辑CuttingCounter预制体,给子物体ProgressBarUI 添加LookAtCamera.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class LookAtCamera : MonoBehaviour
{private enum Mode{LookAt,LookAtInverted,CameraForward,CameraForwardInverted,}[SerializeField] private Mode mode;private void LateUpdate(){switch (mode){case Mode.LookAt: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;}}
}

3、赋值

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

「QT」几何数据类 之 QVector2D 二维向量类

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「QT」QT5程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasolid…

Spring中的过滤器和拦截器

Spring中的过滤器和拦截器 一、引言 在Spring框架中&#xff0c;过滤器&#xff08;Filter&#xff09;和拦截器&#xff08;Interceptor&#xff09;是实现请求处理的两种重要机制。它们都基于AOP&#xff08;面向切面编程&#xff09;思想&#xff0c;用于在请求的生命周期…

网站架构知识之Ansible模块(day021)

1.Ansible模块 作用:通过ansible模块实现批量管理 2.command模块与shell模块 command模块是ansible默认的模块&#xff0c;适用于执行简单的命令&#xff0c;不支持特殊符号 案列01&#xff0c;批量获取主机名 ansible all -m command -a hostname all表示对主机清单所有组…

requestAnimationFrame与setInterval的抉择

&#x1f64c; 如文章有误&#xff0c;恳请评论区指正&#xff0c;谢谢&#xff01; ❤ 写作不易&#xff0c;「点赞」「收藏」「转发」 谢谢支持&#xff01; 背景 在之前的业务中遇到有 JS 动画的实现场景&#xff0c;但当电脑打开太多网页或是同时启动很多应用时&#xff0c…

高性能分布式缓存Redis-分布式锁与布隆过滤器

一、分布式锁 我们先来看一下本地锁 在并发编程中&#xff0c;我们通过锁&#xff0c;来避免由于竞争而造成的数据不一致问题。通常&#xff0c;我们以 synchronized 、Lock 来使用它&#xff08;单机情况&#xff09; 来看这段代码 Autowired RedisTemplate<String,Str…

Flutter运行App时出现“Running Gradle task ‘assembleDebug“问题解决

在参考了众多解决办法中最有用并且最快的方法 Gradle Distributions 在这个地方下载对应你这个文件中的gradle版本 然后将 最后一行本来不是这样的,我们把下载好的zip包保存到本地,然后用这个代替网址,最后成功运行

【CUDA】认识CUDA

目录 一、CUDA编程 二、第一个CUDA程序 三、CUDA关键字 四、device管理 4.1 初始化 4.2 Runtime API查询GPU信息 4.3 决定最佳GPU CUDA C 编程指南CUDA C在线文档&#xff1a;CUDA C 编程指南 CUDA是并行计算的平台和类C编程模型&#xff0c;能很容易的实现并行算法。只…

【优选算法篇】微位至简,数之恢宏——解构 C++ 位运算中的理与美

文章目录 C 位运算详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;位运算基础应用1.1 判断字符是否唯一&#xff08;easy&#xff09;解法&#xff08;位图的思想&#xff09;C 代码实现易错点提示时间复杂度和空间复杂度 1.2 丢失的数字&#xff08;easy&#xff0…

从0开始学习机器学习--Day21--算法的评估标准

准确率和召回率(precision and recall) 在上一章我们提到了在每次运行算法时通过返回一个实数值来判断算法的好坏&#xff0c;但是我们该如何构建这个实数的计算公式呢&#xff0c;毕竟这关乎于我们对算法的判断&#xff0c;不能过于夸大或贬低。有一个典型的会被影响的很大例…

自然语言处理在客户服务中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 自然语言处理在客户服务中的应用 引言 自然语言处理概述 定义…

【Ubuntu24.04】从双系统到虚拟机再到单系统的故事

故事 在大学前期&#xff0c;我使用Ubuntu系统都是为了学习一些命令或者其它Linux的东西&#xff0c;对性能的要求不高&#xff0c;所以选择了虚拟机&#xff0c;后来为了做毕设&#xff0c;选择安装了Ubuntu20.04双系统&#xff0c;因为虚拟机实在带不动&#xff0c;那时我的主…

初次体验Tauri和Sycamore(1)

原创作者&#xff1a;庄晓立&#xff08;LIIGO&#xff09; 原创时间&#xff1a;2024年11月10日 原创链接&#xff1a;https://blog.csdn.net/liigo/article/details/143666827 版权所有&#xff0c;转载请注明出处。 前言 Tauri 2.0发布于2024年10月2日&#xff0c;Sycamore…

【统计子矩阵——部分前缀和+双指针】

题目 代码 #include <bits/stdc.h> using namespace std; typedef long long ll; const int N 510; int s[N][N]; int main() {ios::sync_with_stdio(0);cin.tie(0);int n, m, k;cin >> n >> m >> k;for(int i 1; i < n; i)for(int j 1; j <…

「QT」QT5程序设计专栏目录

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「QT」QT5程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasolid…

Qt学习笔记第41到50讲

第41讲 UI美化遗留问题解决 如上图所示目前记事本的雏形已现&#xff0c;但是还是有待优化&#xff0c;比如右下角的拖动问题。 解决方法&#xff1a; ①首先修改了Widget类的构造函数。 Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) {ui->s…

深度学习经典模型之ZFNet

1 ZFNet 1.1 模型介绍 ​ ZFNet是由 M a t t h e w Matthew Matthew D . Z e i l e r D. Zeiler D.Zeiler和 R o b Rob Rob F e r g u s Fergus Fergus在AlexNet基础上提出的大型卷积网络&#xff0c;在2013年ILSVRC图像分类竞赛中以11.19%的错误率获得冠军&#xff08;实际…

移动应用开发:简易登录页

文章目录 简介一&#xff0c;创建新活动二&#xff0c;设计UI布局三&#xff0c;编写活动代码四&#xff0c;运行应用程序注意 简介 使用Android Studio编写的简单Android 登录应用程序&#xff0c;该应用程序包含一个登录界面&#xff0c;具有账号和密码两个文本框&#xff0…

网络基础:http协议和内外网划分

声明 学习视频来自B站UP主泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频https://space.bilibili.com/350329294 一&#xff0c;H…

英飞凌Aurix2G TC3XX GPT12模块详解

英飞凌Aurix2G TC3XX GPT12模块详解 本文主要介绍英飞凌 Aurix2G TC3XX系列芯片GPT12模块硬件原理、MCAL相关配置和部分代码实现。 文章目录 英飞凌Aurix2G TC3XX GPT12模块详解1 模块介绍2 功能介绍2.1 结构2.2 独立运行模式2.2.1 定时器模式2.2.2 门控定时器模式2.2.3 计数…

大数据程序猿不可不看的资料大全

​ 随着大数据技术的发展&#xff0c;大数据程序猿在数据采集、处理、分析、存储等方面的技能需求不断增加。要在这个领域保持竞争力&#xff0c;系统性地学习和掌握大数据工具、技术架构和行业趋势是非常重要的。以下为您提供一份围绕大数据程序猿不可不看的资料大全&#xf…