如何制作一个Unity2D闯关游戏(三)
回顾上一篇
上一篇我们制作了Player的绝大部分动画,使我们的Player可以真正意义上的动起来,又添加了背景移动,使我们的背景可以一直循环移动,从而使得整个游戏世界更加真实。
开始第三天的制作
在完成了前两篇的内容之后,Player方面的内容我们已经大致完成,接下来我们将开始着重于关卡方面的制作。
当我们浏览完整个Sunny Land素材之后,会发现一个问题,这个素材包只有一个场景包含了关卡的场景内容,但是有许多场景内容的素材,那我们该如何制作属于自己独特的场景呢?这时候我们需要用到Unity内的一个工具来帮助我们完成自定义场景的搭建。
Tilemap的创建
在Unity的选项卡中依次选择 Windows -> 2D -> Tile Palette,打开平铺调色板窗口。然后再如下目录找到 SunnyLandTiles.png,"Assets\2D Platformer Assets\Graphics\Level Art\Tileset",点击 Create New Palette,然后将其拖入 Tile Palette 面板,并保存在合适位置。
创建完成之后,然后我们在依次点击Unity选项卡 GameObject -> 2DObject -> Tilemap -> Rectangular创建一个Gird,然后我们就会发现Scene窗口中就出现了许多格子,现在我们就可以在 Tile Palette 窗口中选择画笔开始绘制场景了。
给Tilemap添加Collider
现在我们在场景中创建好的内容是不具有碰撞的,玩家无法在上面行走,这时候需要我们给Tilemap添加上碰撞体。我们给Tilemap添加上 Tilemap Collider 2D,Rigidbody2D 组件,并且将Rigidbody 2D的 **Body Type **设置为 Kinematic,这样场景就具有了碰撞并且不会受物理效果影响。
但是这时候的碰撞体过多,会造成性能下降,所以这时候我们需要再添加一个 Composite Collider 2D 来解决这个问题,将其的 Geometry Type 设置为 Polygons。
这样场景的碰撞体就从P1变到了P2。
最后要说的一点就是,有些场景元素我们并不需要它具有碰撞,所以我们要对其进行一定的设置。
我们需要将它的 Collider Type 设置为 None。
附上一张说明图,Unity默认给的类型是 Sprite。
以上,自定义场景部分的内容就完成了,我们可以自由的绘制场景中的内容了,创建多个独特的关卡。
同时我们可以将 Player,Background,Main Camera 这三个最基础的元素制作成预设体,方便在多个场景内使用。
创建Spike
场景搭建完成了,我们需要给场景内添加一些尖刺,来增加游戏难度。在素材的 Level Mechanics 文件夹底下找到 Spikes,将它放入场景中,给它添加 Box Collider 2D,将 is Trigger勾上,并将碰撞体设置为图示大小。
然后在创建一个名为 SpikesDamagePlayer 的脚本,用来控制尖刺对玩家造成的伤害。
这个脚本比较简单,直接上代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| using System; using System.Collections; using System.Collections.Generic; using UnityEngine;
public class SpikesDamagePlayer : MonoBehaviour { private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; PlayerHealthController.instance.DealDamage(1); } }
|
当然,我们上面写到的 PlayerHealthController.instance.DealDamage(1);
这个 PlayerHealthController 脚本我们还没有编写,接下来就来编写这一脚本。
玩家血量系统
上面我们写了尖刺对玩家造成伤害的的脚本,接下来我们来写玩家血量系统的脚本。
首先,我们创建一个叫 PlayerHealthController 的脚本,挂载到Player上。玩家血量系统这个脚本肯定会被多个脚本调用到,所以我们在脚本开头就将它定义为静态变量。
1 2 3 4 5 6 7 8
| public static PlayerHealthController instance;
private void Awake() { instance = this; }
|
然后我们需要两个变量来表示玩家当前血量和最大血量。
1 2 3 4 5 6 7 8
| public int currentHealth; public int maxHealth=6;
void Start() { currentHealth = maxHealth; }
|
然后我们就来编写上面用到过得DealDamage函数。
1 2 3 4 5 6 7 8 9 10
| public void DealDamage(int damage) { currentHealth -= damage; if (currentHealth <= 0) { currentHealth = 0; gameObject.SetActive((false)); } }
|
这样一个玩家扣血脚本就写完了,因为后续还要添加新的,所以先不上完整代码。
此处
血量UI显示系统
上面写完了玩家血量系统,那么我们需要UI来将血量信息反馈给玩家,这时候我们需要再场景中创建一个 Canvas,然后将 Canvas 按图示设置。
然后将 Game 窗口的分辨率设置为 Full HD(1920x1080),做一个最初版本的,后期再去适配其他分辨率。
我们创建一个名为 UIController 的脚本,并将其挂载到 Canvas 上。
首先我们同血量系统一样,UI系统也会被多个脚本调用,所以我们也将它定义为静态变量。
1 2 3 4 5 6
| public static UIController instance;
private void Awake() { instance = this; }
|
然后我们需要一个血量的最初模板,所以我们需要一个血量预设体。右键Canvas, Canvas -> UI -> Image,创建一个Image,将他的 Source Image 设置为素材中血量的任意一个,点击 Set Nactive Size,然后将其制成预设体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public Image heartPrefab;
private Image[] heart = new Image[76];
public Sprite fullHeart, emptyHeart,halfHeart;
private Vector2 heartCurrentPosition;
private readonly Vector2 heartStartPosition=new Vector2(-900f, 490f);
private readonly Vector2 delColumnPos = new Vector2(100f, 0);
private readonly Vector2 delRowPos = new Vector2(0, -90f);
|
然后我们在Start函数里编写血量UI的初始化代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| private void Start() { heartCurrentPosition = heartStartPosition; if (PlayerHealthController.instance != null) { for (int i = 0; i < PlayerHealthController.instance.maxHealth/2; i++) { heart[i] = Instantiate(heartPrefab); heart[i].transform.SetParent(transform); var rect = heart[i].GetComponent<RectTransform>(); rect.anchoredPosition = heartCurrentPosition; if ((i + 1) % 19 == 0) { heartCurrentPosition += delRowPos; heartCurrentPosition.x = heartStartPosition.x; } else { heartCurrentPosition += delColumnPos; }
heart[i].sprite = fullHeart; } } }
|
血量初始化完成之后,就要写每次加血或者扣血的UI更新函数了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public void UpdateHeartDisplay() { var cnt = PlayerHealthController.instance.currentHealth; for(int i=0;i<=cnt/2-1;i++) { heart[i].sprite = fullHeart; } for(int i=PlayerHealthController.instance.maxHealth/2-1;i>=cnt/2+1;i--) { heart[i].sprite = emptyHeart; }
if(cnt!=PlayerHealthController.instance.maxHealth)heart[cnt / 2].sprite = cnt % 2 == 1 ? halfHeart : emptyHeart; }
|
UI更新脚本写完之后,我们需要让玩家扣血之后实时更新,所以在刚才的扣血脚本中添加一句代码。
1
| UIController.instance.UpdateHeartDisplay();
|
最后上三个脚本的完整代码
SpikesDamagePlayer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| using System; using System.Collections; using System.Collections.Generic; using UnityEngine;
public class SpikesDamagePlayer : MonoBehaviour { private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; PlayerHealthController.instance.DealDamage(1); } }
|
PlayerHealthController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| using System; using System.Collections; using System.Collections.Generic; using UnityEngine;
public class PlayerHealthController : MonoBehaviour { public static PlayerHealthController instance; public int currentHealth; public int maxHealth=6;
private void Awake() { instance = this; }
void Start() { currentHealth = maxHealth; }
public void DealDamage(int damage) { currentHealth -= damage; UIController.instance.UpdateHeartDisplay(); if (currentHealth <= 0) { currentHealth = 0;
gameObject.SetActive((false)); } } }
|
UIController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI;
public class UIController : MonoBehaviour { public static UIController instance; public Image heartPrefab; private Image[] heart = new Image[76]; public Sprite fullHeart, emptyHeart,halfHeart; private Vector2 heartCurrentPosition; private readonly Vector2 heartStartPosition=new Vector2(-900f, 490f); private readonly Vector2 delColumnPos = new Vector2(100f, 0); private readonly Vector2 delRowPos = new Vector2(0, -90f); private void Awake() { instance = this; }
private void Start() { heartCurrentPosition = heartStartPosition; if (PlayerHealthController.instance != null) { for (int i = 0; i < PlayerHealthController.instance.maxHealth/2; i++) { heart[i] = Instantiate(heartPrefab); heart[i].transform.SetParent(transform); var rect = heart[i].GetComponent<RectTransform>(); rect.anchoredPosition = heartCurrentPosition; if ((i + 1) % 19 == 0) { heartCurrentPosition += delRowPos; heartCurrentPosition.x = heartStartPosition.x; } else { heartCurrentPosition += delColumnPos; }
heart[i].sprite = fullHeart; } } } public void UpdateHeartDisplay() { var cnt = PlayerHealthController.instance.currentHealth; for(int i=0;i<=cnt/2-1;i++) { heart[i].sprite = fullHeart; } for(int i=PlayerHealthController.instance.maxHealth/2-1;i>=cnt/2+1;i--) { heart[i].sprite = emptyHeart; } if(cnt!=PlayerHealthController.instance.maxHealth)heart[cnt / 2].sprite = cnt % 2 == 1 ? halfHeart : emptyHeart; }
}
|
总结
本篇主要添加了自定义场景的制作,Spike尖刺系统,玩家血量系统和血量UI显示系统。有问题欢迎大家在评论区提出,谢谢!