如何制作一个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 面板,并保存在合适位置。
![](https://img1.imgtp.com/2023/08/19/qPfD9fhf.png)
![](https://img1.imgtp.com/2023/08/19/DzlDvq1d.png)
创建完成之后,然后我们在依次点击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。
![](https://img1.imgtp.com/2023/08/19/L2SxwLQM.png)
![](https://img1.imgtp.com/2023/08/19/jUwPl57a.png)
最后要说的一点就是,有些场景元素我们并不需要它具有碰撞,所以我们要对其进行一定的设置。
![](https://img1.imgtp.com/2023/08/19/ktiFcuAF.png)
我们需要将它的 Collider Type 设置为 None。
附上一张说明图,Unity默认给的类型是 Sprite。
![](https://img1.imgtp.com/2023/08/19/XRPR0Ig8.png)
以上,自定义场景部分的内容就完成了,我们可以自由的绘制场景中的内容了,创建多个独特的关卡。
同时我们可以将 Player,Background,Main Camera 这三个最基础的元素制作成预设体,方便在多个场景内使用。
创建Spike
场景搭建完成了,我们需要给场景内添加一些尖刺,来增加游戏难度。在素材的 Level Mechanics 文件夹底下找到 Spikes,将它放入场景中,给它添加 Box Collider 2D,将 is Trigger勾上,并将碰撞体设置为图示大小。
![](https://img1.imgtp.com/2023/08/19/DxW094U5.png)
然后在创建一个名为 SpikesDamagePlayer 的脚本,用来控制尖刺对玩家造成的伤害。
这个脚本比较简单,直接上代码。
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上。玩家血量系统这个脚本肯定会被多个脚本调用到,所以我们在脚本开头就将它定义为静态变量。
//这样在别的脚本我们就可以直接通过PlayerHealthController.instance.函数的方法来调用。
public static PlayerHealthController instance;
//并且在Awake函数中赋值
private void Awake()
{
instance = this;
}
然后我们需要两个变量来表示玩家当前血量和最大血量。
public int currentHealth;//当前血量
public int maxHealth=6;//最大血量
//在start函数中将最大生命值赋给当前生命值
void Start()
{
currentHealth = maxHealth;
}
然后我们就来编写上面用到过得DealDamage函数。
//扣血函数
public void DealDamage(int damage)//需要一个输入值,为当前扣的血量
{
currentHealth -= damage;
if (currentHealth <= 0)//血量小于0,则将玩家隐藏
{
currentHealth = 0;
gameObject.SetActive((false));
}
}
这样一个玩家扣血脚本就写完了,因为后续还要添加新的,所以先不上完整代码。
此处
血量UI显示系统
上面写完了玩家血量系统,那么我们需要UI来将血量信息反馈给玩家,这时候我们需要再场景中创建一个 Canvas,然后将 Canvas 按图示设置。
![](https://img1.imgtp.com/2023/08/19/0XEZaT1I.png)
然后将 Game 窗口的分辨率设置为 Full HD(1920x1080),做一个最初版本的,后期再去适配其他分辨率。
我们创建一个名为 UIController 的脚本,并将其挂载到 Canvas 上。
首先我们同血量系统一样,UI系统也会被多个脚本调用,所以我们也将它定义为静态变量。
public static UIController instance;
private void Awake()
{
instance = this;
}
然后我们需要一个血量的最初模板,所以我们需要一个血量预设体。右键Canvas, Canvas -> UI -> Image,创建一个Image,将他的 Source Image 设置为素材中血量的任意一个,点击 Set Nactive Size,然后将其制成预设体。
//定义一个Image变量来储存刚刚的预设体
public Image heartPrefab;
//开一个Image数组,用来储存血量的Image
private Image[] heart = new Image[76];//这个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的初始化代码。
private void Start()
{
//获取
heartCurrentPosition = heartStartPosition;
//初始化血量
if (PlayerHealthController.instance != null)
{
//写一个For循环来生成爱心
for (int i = 0; i < PlayerHealthController.instance.maxHealth/2; i++)//因为有半心的存在,所以需要最大值/2
{
//生成预设体
heart[i] = Instantiate(heartPrefab);
//设置预设体的父级关系,让他显示在Canvas上
heart[i].transform.SetParent(transform);
//获取RectTransform组件,并设置爱心在画布上的位置
var rect = heart[i].GetComponent<RectTransform>();
rect.anchoredPosition = heartCurrentPosition;
//血量位置修改
if ((i + 1) % 19 == 0)//我设置的大小在第19这里换行,后续适配分辨率也修改这个值即可
{
heartCurrentPosition += delRowPos;
heartCurrentPosition.x = heartStartPosition.x;
}
else//换列
{
heartCurrentPosition += delColumnPos;
}
//然后将其设置为满血
heart[i].sprite = fullHeart;
}
}
}
血量初始化完成之后,就要写每次加血或者扣血的UI更新函数了。
//更新血量UI
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更新脚本写完之后,我们需要让玩家扣血之后实时更新,所以在刚才的扣血脚本中添加一句代码。
UIController.instance.UpdateHeartDisplay();
最后上三个脚本的完整代码
SpikesDamagePlayer
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
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
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;
}
}
}
//更新血量UI
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显示系统。有问题欢迎大家在评论区提出,谢谢!