参考教程
参考教程
目录
准备工作
一、创建项目
二、处理预置文件和设置
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
对象。
接下来,Player
和 BaseCounter
类都实现了 IKitchenObjectParent
接口,并分别提供了它们自己的 GetKitchenObjectFollowTransform
方法的具体实现。在 Player
类中,counterToPoint
是一个 Transform
类型的属性,它被用作返回值。而在 BaseCounter
类中,stove
是另一个 Transform
类型的属性,它被用作返回值。
通过这种方式,在其他地方使用 IKitchenObjectParent
接口的实例时,可以使用 Player
或 BaseCounter
类的实例,并调用 GetKitchenObjectFollowTransform
方法来获取不同的厨房对象的跟随位置(根据具体的实现类)。这样可以实现代码的灵活性和可扩展性。
2、接口脚本的一些规则
-
使用
interface
关键字定义接口。例如:public interface IKitchenObjectParent { }
-
接口的命名应尽量清晰明了,表达出接口的用途和功能。
-
方法的声明只包含方法名、参数列表和返回类型,不包含方法的具体实现。
-
方法的返回类型可以是任意合法的数据类型,也可以是其他接口或类的类型。
-
接口中的方法通常不应包含访问修饰符,因为接口中定义的方法默认为公共(
public
)。 -
接口中可以包含属性、事件和索引器等成员,但通常应尽量保持接口的简洁和清晰。接口的目的是定义一组行为,而不是状态。
-
接口可以继承其他接口,使用冒号(
:
)来表示接口之间的继承关系。例如:public interface IChildInterface : IParentInterface { }
-
类可以实现一个或多个接口,使用冒号(
:
)来表示实现的接口。例如:public class MyClass : IInterface1, IInterface2 { }
-
实现接口的类必须提供接口中的所有成员的具体实现,包括方法、属性和事件等。
-
实现接口的类可以选择性地添加其他成员,但在使用接口的实例时,只能访问接口中定义的成员。
3、实现接口的类:在Unity中实现接口的类遵循以下规则:
- 使用关键字
: MonoBehaviour
或: ScriptableObject
标记类继承自MonoBehaviour
或ScriptableObject
,这是Unity中常用的基类。例如:
public class MyClass : MonoBehaviour, MyInterface {
// 类的实现代码
}
或
public class MyClass : ScriptableObject, MyInterface {
// 类的实现代码
}
-
实现接口的类必须实现接口中定义的所有方法,包括事件的处理方法。
-
可以选择性地覆盖接口中定义的默认方法。
-
可以实现多个接口,只需使用逗号将接口名称分隔开。例如:
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事件,检查是否有订阅者,如果有订阅者,则执行事件处理程序
(3) 解析:
OnButtonClick :
是一个事件(通常定义为 event EventHandler OnButtonClick; 或类似的委托类型)。
?.Invoke(this, EventArgs.Empty) :
使用了空条件运算符(?.)来检查 OnButtonClick 是否为 null。
如果 OnButtonClick 不是 null(即有事件订阅者),则调用 Invoke 方法触发事件。
(this, EventArgs.Empty)
Invoke 方法接受的两个参数,这两个参数会传递给事件处理程序
this:表示事件的发送者(即触发事件的对象)。
EventArgs.Empty:表示一个空的事件数据对象,因为某些事件不需要传递特定的数据。
因此,当 ClickButton 方法在其他方法中被调用时,它会检查 OnButtonClick 事件是否有订阅者。如果有订阅者,则会执行订阅者的事件处理程序,并将 this 和 EventArgs.Empty 作为参数传递给事件处理程序。
假设类中已经定义了 OnButtonClick 事件:
public event EventHandler OnButtonClick;
这样,外部类或对象就可以订阅 OnButtonClick 事件,并在事件触发时执行相应的事件处理程序。例如:
myObject.OnButtonClick += MyEventHandler;
// 事件处理程序
private void MyEventHandler(object sender, EventArgs e)
{
// 处理事件
}
当 ClickButton 方法被调用时,MyEventHandler 会被执行,sender 参数会是触发事件的 myObject,而 e 参数会是 EventArgs.Empty。
(三)委托事件的应用
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 here
if (player.HasKitchenObject())
{
//Player is carrying something
player.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 here
if (player.HasKitchenObject())
{
//Player is carrying something
player.GetKitchenObject().SetKitchenObjectParent(this);
}
else
{
//Player not carrying anything
}
}
else
{
//There is a KitchenObject here
if(player.HasKitchenObject())
{
//Player is carrying something
}
else
{
//Player is not carrying anythinbg
GetKitchenObject().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 here
if (player.HasKitchenObject())
{
//Player is carrying something
player.GetKitchenObject().SetKitchenObjectParent(this);
}
else
{
//Player not carrying anything
}
}
else
{
//There is a KitchenObject here
if (player.HasKitchenObject())
{
//Player is carrying something
}
else
{
//Player is not carrying anythinbg
GetKitchenObject().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 here
if (player.HasKitchenObject())
{
//Player is carrying something
player.GetKitchenObject().SetKitchenObjectParent(this);
}
else
{
//Player not carrying anything
}
}
else
{
//There is a KitchenObject here
if (player.HasKitchenObject())
{
//Player is carrying something
}
else
{
//Player is not carrying anythinbg
GetKitchenObject().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 here
if (player.HasKitchenObject())
{
//Player is carrying something
player.GetKitchenObject().SetKitchenObjectParent(this);
}
else
{
//Player not carrying anything
}
}
else
{
//There is a KitchenObject here
if (player.HasKitchenObject())
{
//Player is carrying something
}
else
{
//Player is not carrying anythinbg
GetKitchenObject().SetKitchenObjectParent(player);
}
}
}
public override void InteractAlternate(Player player)
{
if (HasKitchenObject())
{
//There is a KitchenObject here
GetKitchenObject().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 here
if (player.HasKitchenObject())
{
//Player is carrying something
player.GetKitchenObject().SetKitchenObjectParent(this);
}
else
{
//Player not carrying anything
}
}
else
{
//There is a KitchenObject here
if (player.HasKitchenObject())
{
//Player is carrying something
}
else
{
//Player is not carrying anythinbg
GetKitchenObject().SetKitchenObjectParent(player);
}
}
}
public override void InteractAlternate(Player player)
{
if (HasKitchenObject())
{
//There is a KitchenObject here
GetKitchenObject().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 anything
KitchenObject.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 here
if (player.HasKitchenObject())
{
//Player is carrying something
player.GetKitchenObject().SetKitchenObjectParent(this);
}
else
{
//Player not carrying anything
}
}
else
{
//There is a KitchenObject here
if (player.HasKitchenObject())
{
//Player is carrying something
}
else
{
//Player is not carrying anythinbg
GetKitchenObject().SetKitchenObjectParent(player);
}
}
}
public override void InteractAlternate(Player player)
{
if (HasKitchenObject())
{
//There is a KitchenObject here
KitchenObjectSO 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 here
if (player.HasKitchenObject())
{
//Player is carrying something
if (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO()))
{
//Player carrying something that can be cut
player.GetKitchenObject().SetKitchenObjectParent(this);
}
}
else
{
//Player not carrying anything
}
}
else
{
//There is a KitchenObject here
if (player.HasKitchenObject())
{
//Player is carrying something
}
else
{
//Player is not carrying anythinbg
GetKitchenObject().SetKitchenObjectParent(player);
}
}
}
public override void InteractAlternate(Player player)
{
if (HasKitchenObject() && HasRecipeWithInput(GetKitchenObject().GetKitchenObjectSO()))
{
//There is a KitchenObject here And it can be cut
KitchenObjectSO 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 here
if (player.HasKitchenObject())
{
// player is carrying something
if (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO()))
{
// Player carrying something that can be cut
player.GetKitchenObject().SetKitchenObjectParent(this);
// 初始切割进度
cuttingProgress = 0;
}
}
else
{
// Player not carrying anything
}
}
else
{
// There is a KitchenObject here
if (player.HasKitchenObject())
{
// Player is carrying something
}
else
{
// Player is not carrying anything
GetKitchenObject().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 here
if (player.HasKitchenObject())
{
// player is carrying something
if (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO()))
{
// Player carrying something that can be cut
player.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 here
if (player.HasKitchenObject())
{
// Player is carrying something
}
else
{
// Player is not carrying anything
GetKitchenObject().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());
//
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 here
if (player.HasKitchenObject())
{
// player is carrying something
if (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO()))
{
// Player carrying something that can be cut
player.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 here
if (player.HasKitchenObject())
{
// Player is carrying something
}
else
{
// Player is not carrying anything
GetKitchenObject().SetKitchenObjectParent(player);
}
}
}
public override void InteractAlternate(Player player)
{
if (HasKitchenObject() && HasRecipeWithInput(GetKitchenObject().GetKitchenObjectSO()))
{
// There is a KitchenObject her and it can be cut
cuttingProgress++;
// 委托事件
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、赋值