如何使用Unity开发另一个平台游戏。 另一个教程

哈Ha!


另一篇文章正等待您的切入,这将根据Habr上名为2D游戏的关卡设计模式的文章的翻译,向您介绍我如何设定编程游戏的目标。


这篇文章有很多文本(常规和来源)和许多图片。


在开始我的第一篇文章之前,让我们认识您。 我叫丹尼斯。 我以系统管理员的身份工作,总共有7年的经验。 我不是要告诉您的是,系统管理员是一种IT员工,需要认真部署一次,然后考虑在监视器上闪烁各种字符。 随着时间的流逝,我得出的结论是,该是扩展知识范围并转向编程的时候了。 在不赘述的情况下,我尝试使用C ++和Python进行任何项目。 但是经过一年的学习,我得出的结论是,对我的应用程序和系统软件进行编程不是我的。 由于各种原因。


经过更深入的思考,我问自己一个问题:我真的喜欢使用不同类型的计算设备做什么? 我对自己的问题让我进入了孩提时代,也就是在PS1,PS2,PC上的Railroad Tycoon 3花费的欢乐时光中……,嗯,您知道的。 电子游戏!


根据各种培训材料的数量,选择权就落在了Unity上(不重新发明轮子吗?)。 经过一个月的阅读和阅读各种材料后,我决定将第一款非常简单的儿童游戏发布到游戏市场。 克服恐惧可以这么说。 毕竟,在游戏市场发布应用程序并不可怕,对吗?


几个月后,我发布了已经更加复杂的平台程序。 然后休息了(毕竟,必须继续进行这项工作)。


大约两周前,我在一个名为2D游戏的关卡设计模式( https://habr.com/en/post/456152/ )的中心上看到一篇文章的翻译,并自言自语-为什么不呢? 本文的表简单明了,列出了游戏中应该包含的内容,因此很有趣。 我在OneNote中将表复制给自己,并用标签Cases(可以标记为完成)标记每个模式。


结果我想得到什么? 您的批评。 就像我对自己说的那样-如果您想学习游泳,那就用头潜水。 如果您认为我做得很好,请在评论中写信给我。 如果您认为我做得不好,请加倍写下。


我将开始我的长程序,以对另一个平台程序进行编程。


头像


游戏中玩家控制的实体。 例如,《超级马里奥兄弟》(任天堂,1985年)中的马里奥和路易吉。


为了赋予英雄生命,需要执行几个子任务。 即:


•   ( ) •      •      •       •        

要实现动画,我们需要将单个子画面转换为多子画面。 这非常简单。 将精灵添加到项目文件夹,然后在Explorer Explorer Unity Unity Explorer中找到它。 然后,在检查器窗口中单击精灵,将SptiteMode属性的值从Single更改为Multiple


图片


单击“ 应用” ,然后单击“ SpriteEditor”


在“ 精灵编辑器”窗口中,您需要用鼠标选择未来动画的每一帧,如下图所示。


同样,Unity提供了自动突出显示精灵内对象边界的功能。 为此,在“ 精灵编辑器”窗口中,必须单击“ 切片”按钮。 在下拉菜单中,您应该具有Type => Automatic,Pivot => Center 。 您所要做的就是单击“ 切片”按钮。 之后,将自动选择精灵内的所有对象。



让我们对所有其他动画执行此操作。 接下来,您将需要配置动画状态及其切换。 这分两个步骤完成。 第一个动作,程序代码。
创建一个空的游戏对象。 为此,请在“ 层次结构”选项卡中单击鼠标右键,然后从下拉菜单中选择“ 创建空白 ”。



默认情况下,在舞台上创建的一个空游戏对象只有一个组件-Transform 。 该组件确定对象在舞台上的位置,倾斜角度及其比例。


您可以在两种不同的含义中遇到“ 变换 ”一词:


  • 变换是一类。 由于这是一个类,因此Transform描述了该对象将位于什么坐标以及它将位于什么尺寸的软件实现
  • transform是类的实例。 也就是说, 您可以引用特定的对象并更改其在场景中的位置或比例。 例如,在代码的更深处将有一行:
     transform.position = new Vector2 (transform.position.x + dirX, transform.position.y + dirY);. 


    这条线将负责卢卡斯在舞台上的移动。

要创建自己的组件,请在对象检查器的选项卡中单击“ 添加组件”按钮。 接下来,在标准组件中出现一个搜索框。 开始键入将来脚本(或已实现的组件)的名称就足够了,如果没有合适的名称,Unity将为您提供一个创建新组件的名称。 我称这个组件为HeroScript.cs



首先,我们描述将存储有关卢卡斯视觉和物理组件信息的字段:


扰流板方向
 private Animator animator; //      . private Rigidbody2D rb2d; //rb     

接下来,将响应角色移动的字段:


扰流板方向
 /*  ,     */ Vector3 localScale; bool facingRight = true; [SerializeField] private float dirX, dirY; //    [Range(1f, 20f)] [SerializeField] private float moveSpeed; //    private SpriteRenderer sprite; //   SpriteRenderer /*   ,     */ 

一个很好的开始。 接下来,将描述枚举并写入将负责切换动画状态的属性。 该枚举必须在类之外编写:


扰流板方向
 /* *      . *         *    . */ public enum CharState { idle, //   0 Run, //   1 Walk, //   2 Die //   3 } 

我们实现了一个属性,该属性将接收并设置新的动画状态:


扰流板方向
 public CharState State { get {//    CharState    animator       int return (CharState)animator.GetInteger("State"); } set { //    animator     int    State       ,      int. animator.SetInteger("State", (int)value); } } 

软件部分完成。 现在,我们有了一个枚举和一个与切换动画相关的属性。 接下来,第二步。 在Unity编辑器中,您需要绑定动画状态并指出需要更改哪些int值。


为此,您必须将先前创建的多重精灵与一个空的游戏对象相关联。 您需要做的就是在Unity Explorer中选择框架并将其拖动到一个空的游戏对象上,我们之前已将该脚本固定到该游戏对象上。


图片


对每个后续动画执行此操作。 此外,在带有动画的资源管理器中,您将找到带有框图和“ 播放”按钮的对象的外观。 双击它会打开“ Animator”选项卡。 在内部,您将看到几个带有动画的块,并且最初,只有Entry状态和已连接的第一组动画被连接。 AnyState和其他动画将显示为常规灰色正方形。 为了绑定所有内容,您需要单击AnyState的状态,并选择唯一的Make Transaction下拉菜单,并将其绑定到灰色块。 必须针对每种条件执行此操作。 最后,您应该获得类似下面的屏幕快照中所示的内容。


图片


接下来,您必须明确指出State的确切位置,以启动必要的动画。 注意屏幕截图,即其左侧部分。 参数标签。 在其中创建一个类型为int State的变量。 接下来,注意右侧。 首先,从动画过渡开始,您需要取消选中“ 可以向自身交易”复选框。 此操作将使您免于动画向自身和“ 条件”部分的奇怪的过渡,有时甚至是完全无法理解的过渡,在“ 条件”部分中,我们指出此动画过渡被分配了State变量的值3。 之后,Unity将知道要运行哪个动画。
动画角色移动已完成所有操作。 让我们继续前进。


下一步是教卢卡斯走上舞台。 这完全是编程。 要在场景中四处移动角色,您将需要按钮,单击此按钮,卢卡斯将来回移动。 为此,在Assets Store选项卡中,我们需要导入Standart Assets,但不是全部,仅导入一些其他组件,即:


•CrossPlatformInput
•编辑器
•环境


导入资产后,应修改Unity主窗口,并出现另一个选项卡Mobile Input 。 我们激活它。


让我们在舞台上创建新的UI元素-控制按钮。 在每个方向上创建4个按钮。 上,下,前进和后退。 在图像组件中,我们为按钮分配一个图像,该图像将与图像相对应,这意味着可以移动。 它看起来应类似于以下屏幕截图:



向每个按钮添加一个AxisTouchButton组件。 该脚本只有4个字段。 axisName字段表示调用时要响应的名称。 axisValue字段负责Lucas的移动方向。 responseSpeed字段负责Lucas将以多快的速度发展。 returnToCentreSpeed字段负责按钮返回中心的速度。 对于“前进”按钮,保持原样。 对于后退按钮,将axisValue的值更改为-1,以便Lucas向后移动。 对于“向上”和“向下”按钮,将axisName更改为Vertical 。 对于“向上”按钮axisValue,将“向下-1”的值设置为1。


接下来,修改HeroScript.cs 。 将名称空间添加到using指令


 using UnityStandardAssets.CrossPlatformInput; //    . 

扰流板方向
        : /*  ,     */ Vector3 localScale; //   bool facingRight = true; [SerializeField] private float dirX, dirY; //    [Range(1f, 20f)] [SerializeField] private float moveSpeed; //    private SpriteRenderer sprite; //   SpriteRenderer /*   ,     */ 

在标准的Start方法中,添加以下代码:


扰流板方向
  void Start() { localScale = transform.localScale; animator = GetComponent<Animator>(); //     . sprite = GetComponent<SpriteRenderer>(); //  SpriteRenderer rb = GetComponent<Rigidbody2D>(); State = CharState.idle; } 

我们创建一个负责移动英雄的方法:


扰流板方向
 public void MoveHero() { dirX = CrossPlatformInputManager.GetAxis ("Horizontal") * moveSpeed * Time.deltaTime; dirY = CrossPlatformInputManager.GetAxis ("Vertical") * moveSpeed * Time.deltaTime; transform.position = new Vector2 (transform.position.x + dirX, transform.position.y + dirY); } 

如您所见,一切都很简单。 dirXDirY字段记录有关轴方向( 水平垂直 ),速度(需要在编辑器中指定),乘以最后一帧的行进时间的信息。
transform.position将新位置写入游戏对象的Transform组件。


在问题的实际方面,您可以运行场景并查看卢卡斯如何掉入深渊,因为其下没有物体可以阻止这种情况。 卢卡斯(Lucas)始终处在“ 空闲”动画中,当我们将其引导回去时不会转身。 为此,需要修改脚本。 创建一个确定卢卡斯朝哪个方向看的方法:


扰流板方向
 void CheckWhereToFace () { if (dirX > 0) { facingRight = true; State = CharState.Walk; } if (dirX < 0) { facingRight = false; State = CharState.Walk; } if (dirX == 0) { State = CharState.idle; } if (dirY < 0) { State = CharState.Walk; } if (dirY > 0) { State = CharState.Walk; } if (((facingRight) && (localScale.x < 0)) || ((!facingRight) && (localScale.x > 0))) localScale.x *= -1; transform.localScale = localScale; 

这部分代码也不难。 该方法描述了如果dirX > 0(如果我们转到右侧),则我们将精灵沿该方向旋转并开始行走动画。 如果小于0,则将Lucas旋转180度并开始动画。 如果dirX为零,则卢卡斯站着,您需要启动等待动画。


为什么在这种情况下使用Scale操作比使用flipX = true更可取 ? 将来,我将描述手拿任何物体的能力,而卢卡斯自然可以转过身来拿手中的东西。 如果我使用通常的反射,那么当卢卡斯向左看时,握在手中的对象将保留在右侧(例如)。 放大将使固定卢卡斯的对象朝与卢卡斯本人转过的方向相同的方向移动。


我们将CheckWhereToFace()函数放在Update()函数中,以便逐帧监视。


太好了 5的前2分完成。 让我们继续对卢卡斯的需求。 假设卢卡斯必须具备3种生存需求。 这是生活水平,饥饿水平和口渴水平​​。 为此,您需要创建一个简单易懂的面板,其中包含每个项目的指示器。 要创建这样的面板,请右键单击并选择UI => Panel


让我们大致如下标记


图片


该面板包含每个需求的三幅图像(图像)(左)。 右边是面板本身。 在第一层(我们将以这种方式进行说明)上,有一个不透明的颜色指示器(图像),在其下面复制了一个Image对象,该对象是透明的。 此图像是原始图像的一半透明。 此外,不具有透明度的图像具有图像类型=填充属性。 此功能将使我们能够模拟需求规模的减少。


图片


图片


定义新的静态变量:


扰流板方向
 /*  ,     */ [SerializeField] public static float Health = 100, Eat = 100, Water = 100, _Eat = 0.05f, _Water = 0.1f; //        .    _  . /*   ,     */ /*  ,      */ [SerializeField] Image iHealt, iEat, iWater; //      /*   ,      */ 

在这种情况下,我使用静态字段。 这样做是为了使这些字段对于整个类都是唯一的。 同样,这将使我们能够通过类名直接访问这些字段。 我们编写一些简单的函数:


扰流板方向
 private float fEat(float x) { Eat = Eat - x * Time.deltaTime; iEat.fillAmount = Eat / 100f; // ,        return Eat; } private float fWater(float x) { Water = Water - x * Time.deltaTime; iWater.fillAmount = Water / 100; return Water; } 

然后,我们编写一种方法来收集有关饮食欲望的信息:


扰流板方向
 private void Needs() { if (fEat(_Eat) < 0) { Debug.Log(Eat); } else if (fEat(0) == 0) { StartCoroutine(ifDie()); } if (fWater(_Water) < 0) { Debug.Log(Water); } else if (fWater(0) == 0) { StartCoroutine(ifDie()); } 

Needs()函数放置在Update()函数中,并调用每个框架。 因此,在行中


 if (fEat(_Eat) < 0) 

调用一个函数,该函数将应从EatWater变量中获取多少作为参数。 如果函数的结果不为0,则表示卢卡斯尚未因口渴或饥饿而死亡。 如果卢卡斯确实死于饥饿或致命伤,那么我们会协程


 StartCoroutine(ifDie()); 

这将启动死亡动画并重新启动关卡:


扰流板方向
 IEnumerator ifDie() { State = CharState.Die; yield return new WaitForSeconds(2); SceneManager.LoadScene("WoodDay", LoadSceneMode.Single); } 

硬瓦


不允许玩家通过的游戏对象。 例如:《超级马里奥兄弟》中的性别(任天堂,1985年)。


为了实现地球并防止Lucas跌落,您需要将BoxCollider2DRigidbody2D组件连接到Lucas。 另外,您还需要BoxCollider2D组件所在的地球精灵。 BoxCollider2D组件实现了碰撞器及其碰撞行为。 在这个阶段,我们除了防止卢卡斯(Lucas)地下的失败外,什么都不需要。 我们可以选择编辑的仅仅是对撞机的边框。 在我的情况下,地面精灵具有草皮表面,因此看起来草皮无法支撑卢卡斯的重量,我将编辑组件的边框。



现在,一个令人兴奋的关卡标记过程。 为了方便起见,您可以将这块土地出口到预制件。 预制件是游戏对象的容器,对其进行修改后,您可以将更改自动应用于从该预制件创建的所有游戏对象。 接下来,使用CTRL + D克隆此预制件(在“层次结构”选项卡中选择它),然后将其放置在舞台上。


图片


萤幕


玩家当前可见的游戏级别/世界的一部分。


设置一个摄像机,该摄像机将跟随播放器显示部分场景。 接下来,将有一个非常简单的脚本来实现:


扰流板方向
 public GameObject objectToFollow; public float speed = 2.0f; void Update () { CamFoll(); } private void CamFoll() { float interpolation = speed * Time.deltaTime; Vector3 position = this.transform.position; position.y = Mathf.Lerp(this.transform.position.y, objectToFollow.transform.position.y, interpolation); position.x = Mathf.Lerp(this.transform.position.x, objectToFollow.transform.position.x, interpolation); this.transform.position = position; } 

GameObject类型的objectToFollow字段中,将分配一个要监视的对象,在speed字段中,必须平滑地移动到分配的GameObject后面的速度。


自上一帧以来的移动速度信息记录在插值字段中。 接下来,将使用Lerp方法,这将确保相机在沿X和U轴移动时在Lucas后面平滑移动。不幸的是,我无法解释这条线的操作


 position.y = Mathf.Lerp(this.transform.position.y, objectToFollow.transform.position.y, interpolation); 

在数学方面。 因此,我会说更简单-此方法将延长任何操作的执行时间。 在我们的情况下,这是摄像机在物体后面的移动。


危险性


扰流板方向

阻止玩家完成任务的实体。 示例:来自1001个尖峰的尖峰(Nicalis和8bits Fanatics,2014年)。


让我们开始添加一些东西,这不仅会阻止卢卡斯走到舞台的尽头,而且会影响卢卡斯的生命和死亡的可能性(其中一个,我们将实现第五个子问题来实现卢卡斯的人生故事-英雄可以被杀死或死亡)。
在这种情况下,我们在隐藏在植被后面的舞台上散布尖峰,只有玩家的专心才能通过。


创建一个空的GameObject并将SpriteRendererPolygonCollider2D组件连接到它。 在SpriteRenderer组件中, 我们根据需要连接带倒钩的按钮的精灵或任何其他对象。 另外,将标签= Thorn分配给峰值。


接下来,在Lucas GameObject上,我们创建一个脚本,该脚本负责Lucas与其他对撞机发生碰撞时将发生的事情。 就我而言,我将其称为ColliderReaction.cs


扰流板方向
 private Rigidbody2D rb2d; void Start() { rb2d = GetComponent<Rigidbody2D>(); } public void OnTriggerEnter2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Thorn": { rb2d.AddForce(transform.up * 4, ForceMode2D.Impulse); HeroScript.Health = HeroScript.Health - 5; } break; } } 

该脚本的本质很简单,只有2x2。 当Thorn标签游戏对象碰撞时, Switch语句将与我们指定的候选对象进行比较。 就我们而言,现在是Thorn 。 首先,卢卡斯吐了,然后我们转向一个静态变量,并从卢卡斯得到5个生命单位。 展望未来,我可以说描述与敌人冲突的同一件事是有意义的:


扰流板方向
 case "Enemy": { rb2d.AddForce(transform.up * 2, ForceMode2D.Impulse); HeroScript.Health = HeroScript.Health - 10; } break; 

接下来,我建议用一块石头杀死两只鸟。


收集的项目和规则。


玩家可以拾取的游戏对象。


我们提出的规则是,如果卢卡斯想在岛屿之间穿行并爬上去,那么您需要收集一棵树来建造桥梁和楼梯。
根据已经通过的方法,我们将创建一棵树和楼梯。


我们会将脚本连接到树上,该脚本将负责您开始砍伐后可以从中敲出多少条日志。 由于在精灵集中仅提出了攻击的动画,因此我们在切割树时将使用它(生产成本)。
树上的脚本:


扰流板方向
 [SerializeField] private Transform inst; //     [SerializeField] private GameObject FireWoodPref; //   [SerializeField] private int fireWood; //        

当级别开始时,我们在fireWood中写入一个随机值:


扰流板方向
 void Awake() { fireWood = Random.Range(4,10); } 

描述一种带有参数的方法,该方法将负责一次笔触将落入多少日志:


扰流板方向
 public int fireWoodCounter(int x) { for (int i = 0; i < fireWood; i++) { fireWood = fireWood - x; InstantiateFireWood(); } return fireWood; } 

一种在舞台上创建日志克隆的方法。
私有void InstantiateFireWood():


扰流板方向
  { Instantiate(FireWoodPref, inst.position, inst.rotation); } 


让我们创建一个日志,并将脚本与以下代码连接:


扰流板方向
 public void OnTriggerEnter2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Player": { if (InventoryOnHero.woodCount > 10) { Debug.Log("   !"); } else { InventoryOnHero.woodCount = InventoryOnHero.woodCount + 1; Destroy(this.gameObject); } } break; } } 

接下来,我们还将创建一个负责库存的类。


首先,检查袋子中是否有空间。 如果没有,那么错误和日志仍然存在,如果有空间,那么我们将库存补充一个单位并销毁日志。
接下来,您需要使用这些资源。 如上所述,我们为玩家提供了建造桥梁和楼梯的机会。


要创建桥,我们需要两个预制件,桥的左右一半。 BoxCollider2D . , , - , .


:


扰流板方向
 [SerializeField] private Transform inst1, inst2; //        [SerializeField] private GameObject bridgePref1, bridgePref2; //   [SerializeField] private int BridgeCount; //   ,   .    

:


扰流板方向
 public void BuildBridge() { if (InventoryOnHero.woodCount == 0) { Debug.LogWarning (" !"); } if (InventoryOnHero.woodCount > 0) { BridgeCount = BridgeCount - 1; InventoryOnHero.woodCount = InventoryOnHero.woodCount - 1; } switch (BridgeCount) { case 5: Inst1(); break; case 0: Inst2(); break; default: Debug.LogWarning("-      "); break; } } 

, , . , 10 , 12 8.


, , , , . , 1 , 1 . , 5, , . 0, . , .


.


, ColliderReaction.cs :


扰流板方向
 void OnTriggerStay2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Ladder": { rb2d.gravityScale = 0; } break; } } void OnTriggerExit2D(Collider2D collision) { switch (collision.gameObject.tag) { case "Ladder": { rb2d.gravityScale = 1; } break; } } 

OnTriggerStay2D , . , 0. , . OnTriggerExit2D , .



, .


19 , . , , , , , .


GO, SpriteRenderer , BoxCollider2D , Rigidbody2D . , — , . , ru.stackoverflow.com.


图片


Trees .


, . , -, , Raycast 2 (4 ). , , , ( ). ( ), . , . , , . , . , , ( , ).


, . , - .


:


扰流板方向
 [SerializeField] private GameObject area; private bool m1 = true, m2; // m  move private void fGreenMonster() { float dist = Vector3.Distance(greenMonster.transform.position, area.transform.position); Debug.Log(dist); if (m1) { if (dist < 3f) { transform.position += new Vector3(speed,0,0) * Time.deltaTime; SR.flipX = true; } else { m1 = false; m2 = true; } } if (m2) { if(dist >= 1f) { transform.position += new Vector3(-speed,0,0) * Time.deltaTime; SR.flipX = false; } else { m2 = false; m1 = true; } } } 

Update() , . , 3 , . 3, , .


图片


, .


扰流板方向
 private void fSunFlower() { canBullet = canBullet - minus * Time.deltaTime; if (canBullet <= 0 && SR.flipX == false) { GameObject newArrow = Instantiate(sunFlowerBullet) as GameObject; newArrow.transform.position = transform.position; Rigidbody2D rb = newArrow.GetComponent<Rigidbody2D>(); rb.velocity = sunFlowerTrans.transform.forward * -sunFlowerBulletSpeed; canBullet = 2; } if (canBullet <= 0 && SR.flipX == true) { GameObject newArrow = Instantiate(sunFlowerBullet) as GameObject; newArrow.transform.position = transform.position; Rigidbody2D rb = newArrow.GetComponent<Rigidbody2D>(); rb.velocity = sunFlowerTrans.transform.forward * sunFlowerBulletSpeed; canBullet = 2; } 

 canBullet = canBullet - minus * Time.deltaTime; 

, .


扰流板方向
 if (canBullet <= 0 && SR.flipX == false) { GameObject newArrow = Instantiate(sunFlowerBullet) } 

, , , , :


扰流板方向
 public int Damage(int x) { Health = Health - x; return Health; } 

, , :


扰流板方向
 public void ifDie() { if (Damage(0) <= 0) { Destroy(this.gameObject); } } 

0, .


, :


扰流板方向
 if (bGreenMonster) { fGreenMonster(); } if (bSunFlower) { fSunFlower(); } 

, .


图片


.


, ?


, .


:



:


扰流板方向
 [SerializeField] private Transform Hero; //         [SerializeField] private float distWhatHeroSee; //   [SerializeField] private LayerMask Tree, BridgeBuild, LadderBuild ,drinkingWater, lEnemy; //   

, :


扰流板方向
 private void AttackBtn() { if (CrossPlatformInputManager.GetButtonDown("Attack")) { GameObject.Find("Hero").GetComponent<HeroScript>().State = CharState.AttackA; Collider2D[] Trees = Physics2D.OverlapCircleAll(Hero.position, distWhatHeroSee, Tree); for (int i = 0; i < Trees.Length; i++) { Trees[i].GetComponent<TreeControl>().fireWoodCounter(1); Debug.Log("Trees Collider"); HeroScript.Water = HeroScript.Water - 0.7f; } // BB  BridgeBuild Collider2D[] BB = Physics2D.OverlapCircleAll(Hero.position, distWhatHeroSee, BridgeBuild); for (int i = 0; i < BB.Length; i++) { BB[i].GetComponent<BridgeBuilding>().BuildBridge(); HeroScript.Water = HeroScript.Water - 0.17f; } 

 GameObject.Find("Hero").GetComponent<HeroScript>().State = CharState.AttackA; 

, .
, :


扰流板方向
 Collider2D[] Trees = Physics2D.OverlapCircleAll(Hero.position, distWhatHeroSee, Tree); for (int i = 0; i < Trees.Length; i++) { Trees[i].GetComponent<TreeControl>().fireWoodCounter(1); Debug.Log("Trees Collider"); HeroScript.Water = HeroScript.Water - 0.7f; } 

Trees , . , , , . .
, . Simple as that!


, - :


图片


, — .
, . , , , .


2 , .


!


.


https://opengameart.org/ , :


Source: https://habr.com/ru/post/zh-CN458846/


All Articles