查看原文
其他

0基础项目全流程实战!Code Monkey 2023系列基础课程中文版持续更新中

Code Monkey Unity官方平台 2023-11-09
Unity 开发者社区大佬 Code Monkey 2023 年推出了系列基础免费课程《Code Monkey 最新教程 2023》,该系列课程总计约 10 小时,将带你从头开始制作一个完整的街机游戏!该游戏具有许多有趣的玩法系统、关卡设计,制作该游戏过程中所学到的知识,将能够适用于构建几乎任何类型的游戏。
这套教程适用于想要学习如何编写高质量代码,并学习如何管理好复杂项目的初学者和中级开发者。我们正在将全套视频汉化至 Unity 中文课堂,点击阅读原文,立即开始学习吧!

本文节选这套教程的其中一节,Code Monkey 将带大家通过状态机实现一个可以煎肉的炉灶,根据时间条呈现生肉—煎熟—烤焦三种状态。

课代表笔记

  炉灶预制件

首先制作一个柜台,创建一个柜台的预制件变体,并拖入 StoveCounter_Visual。这里还需要复制一遍这一预制件变体,并为其中的 Counter、Stove 和 Frying Pan 换上 CounterSelected 偏白色的材质,用于表示对象被选中。

在 Counters 脚本文件夹中新建 StoveCounter 脚本,是在此前课程中 BaseCounter 脚本的基础上扩展的。炉灶柜脚本的特点是,它要围绕计时器运作,根据时间进度及与角色的互动呈现肉的三种状态。
用一个 FryingRecipeScriptableObject - MeatPattyUncooked-MeatPattycooked 储存输入、输出与煎炸计时器,添加 MeatPattyUncooked(生肉)、MeatPattyCooked(熟肉)、MeatPattyBurned(焦肉)的 SpriptableObject 和预制件,然后将 MeatPattyUncooked-MeatPattycooked 可编程对象中的输入选为 MeatPattyUncooked,输出选为 MeatPattyCooked,并设置计时器的最长时间为 3 秒。
  根据计时器煎肉
继续编写脚本,使炉灶柜能在与玩家互动时,检验玩家放上去的物品是否符合煎炸菜谱。只有能被煎炸的物品才能被放置在炉灶柜上。
public class StoveCounter : BaseCounter {
[SerializeField] private FryingRecipeSO[] fryingRecipeSOArray;
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 Fried          player.GetKitchenObject().SetKitchenObjectParent(this);
fryingRecipeSO = GetFryingRecipeSOWithInput(GetKitchenObject().GetKitchenObjectSO()); } } 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); } } }      

煎炸计时器有两种写法,一种是简单的浮点计时器,当计时器的时间大于此前设定的最大值,肉就煎好了:

private float fryingTimer;private FryingRecipeSO fryingRecipeSO;
private void Update() { if (HasKitchenObject()) { fryingTimer += Time.deltaTime; if (fryingTimer > fryingRecipeSO.fryingTimerMax) { // Fried fryingTimer = of; Debug.Log("Fried!"); GetKitchenObject().DestroySelf();
KitchenObject.SpawnKitchenObject(fryingRecipeSO.output,this); } Debug. Log(fryingTimer);} }}
另一种是用协程(Coroutine):
private void Start() { StartCoroutine(HandleFryTimer());}
private IEnumerator HandleFryTimer () {    yield return new WaitForSeconds(1f);}
  生肉-煎熟-烤焦
我们希望炉灶先煎生肉,煎熟之后再实现把它烤焦的效果,因此我们需要做个简单的状态机。我们在上边写下 private enum,称之为 State,然后把状态机的状态都放进去。
private enum State { Idle, Frying, Fried, Burned,}
在编写状态机时,首先要储存当前状态,然后在 Update() 上用 switch 循环当前状态,并根据当前所处状态执行不同的逻辑。
把上文写的依计时器煎炸的逻辑嵌入状态机里:
private State state;private float fryingTimer;private FryingRecipeSO fryingRecipeSO;
private void Start() {  state = State.Idle;}
private void Update() { if (HasKitchenObject ()) { switch (state) { case State.Idle: break; case State.Frying: fryingTimer += Time.deltaTime;        if (fryingTimer › fryingRecipeSO.fryingTimerMax) { // Fried GetKitchenObject () .DestroySelf();
KitchenObject.SpawnKitchenObject(fryingRecipeSO.output,this);
Debug. Log("Object fried!");          state = State. Fried; } break; case State.Fried: break; case State. Burned: break;    }  Debug. Log (state);  }}

并在玩家与炉灶互动的逻辑中也添加上,如果放下的物品是可以煎炸的,就把状态设为 State.Frying,并且重置计时器。

if (HasRecipeWithInput(player.GetKitchenObject().GetKitchenObjectSO())) { // Player carrying something that can be Fried player.GetKitchenObject().SetKitchenObjectParent(this);
fryingRecipeSO = GetFryingRecipeSOWithInput(GetKitchenObject().GetKitchenObjectSO());
state = State.Frying; fryingTimer = Of;}

继续制作烤焦状态,与煎炸状态的逻辑类似,只需创建一个新的 FryingTimer 计时器,及 BurningRecipeSO 可编程对象 MeatPattyCooked-MeatPattyBurned,将输入与输出分别配置为 MeatPattyCooked 和 MeatPattyBurned,计时器最长时间设为 5 秒。

state = State. Fried; burningTimer = Of; burningRecipeSO = GetBurningRecipesOWithInput(GetKitchenObject () .GetKitchenObjectSO ()); } break;case State.Fried: burningTimer += Time.deltaTime; if (burningTimer > burningRecipeSO.burningTimerMax) { GetKitchenObject ().DestroySelf();
KitchenObject.SpawnKitchenObject(burningRecipesO.output,this);
Debug.Log("Object Burned!"); state = State.Burned; }
  火炉效果

添加火炉开启的图像预制件 StoveOnVisual (加上泛光 Bloom 效果),并用粒子系统制作火星的粒子效果 SizzlingParticles。

为它们新建脚本 StoveCounterVisual,以实现炉子开启时就出现发光及火星的效果。这里用事件 OnStageChanged 来触发效果,每当煎肉炉的状态变化,就触发该事件;当状态为 Frying 或 Fried 时,就显示这两个效果对象。

public class StoveCounterVisual : MonoBehaviour {
[SerializeField] private StoveCounter stoveCounter; [SerializeField] private GameObject stoveOnGameObject; [SerializeField] private GameObject particlesGameObject;
private void Start() { stoveCounter.OnStateChanged += StoveCounter_OnStateChanged;  } private void StoveCounter_OnStateChanged(object sender, StoveCounter.OnStateChangedEventArgs e) { bool showVisual = e.state == StoveCounter.State.Frying || e.state == StoveCounter.State. Fried; stoveOnGameObject.SetActive(showVisual); particlesGameObject. SetActive(showVisual);  }
}

  进度条

由于在课程前序的切菜柜一节中也有进度条的设置,为了避免代码重复,写出整洁的代码,这里创建一个接口,应用到任何带有进度的类上。创建脚本 IHasProgress:
public interface IHasProgress {
public event EventHandler<OnProgressChangedEventArgs> OnProgressChanged; public class OnProgressChangedEventArgs : EventArgs { public float progressNormalized; }
}

将其应用到炉灶柜上,把 ProgressBarUI 预制件拖入到 StoveCounter 预制件,再把 StoveCounter 拖入 ProgressBarUI 脚本中的 Has Progress Game Object 字段。在 StoveCounter 脚本中应用这个接口:

public class StoveCounter: BaseCounter, IHasProgress {public event EventHandler<IHasProgress.OnProgressChangedEventArgs>OnProgressChanged;

在每次进入 Frying 和 Fried 状态时触发这个事件,修改进度量,并显示进度条。

OnProgressChanged?. Invoke (this, new IHasProgress. OnProgressChangedEventArgs {  progressNormalized = fryingTimer / fryingRecipeSO.fryingTimerMax});

在 Burned 状态下隐藏进度条。当玩家中途拿起食材时,状态重置,进度条隐藏。

OnProgressChanged?. Invoke(this, new IHasProgress.OnProgressChangedEventArgs { progressNormalized = 0f});

这套教程目前已更新至第 30 课,完成了一款烹饪游戏中厨房物品、切菜柜、垃圾柜、炉灶柜、盘子等系统和功能的制作。点击阅读原文到 Unity 中文课堂立即免费学习吧!




长按关注

Unity 官方微信

第一时间了解Unity引擎动向,学习进阶开发技能





 点击“阅读原文”,前往课程主页 


继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存