零、前言
最近无意中发现了《Unbound: Worlds Apart》游离于世界之海这款游戏,尤其欣赏其中魔术门双重世界的效果,正当作者想尝试实现下时,奈何在网上找了很久都没有找到对应效果的实现教程(可能是作者找得不够仔细),无奈之下作者只好自己摸索尝试。
一、简单介绍游戏及效果
《Unbound: Worlds Apart》是一款类《奥日》风格的2D平台冒险独立游戏。游戏中,玩家作为一名魔术师,可以通过召唤魔术门从而实现在不同的现实之间穿梭。每一道魔术门,可以体验到一个个不同的世界,而每个世界都有其独特的运行法则,比如反重力、时间控制,以及超级力量等等。
二、简单分析如何实现魔术门双重世界效果
2.1操作
玩家在游玩过程中,可以通过按键在当前位置生成一道魔术门。
2.2魔术门内外世界
魔术门的内外世界是通过SpriteMask遮罩实现的。场景中存在两重的世界,当玩家按下按键后,在玩家当前位置生成或激活一个指定半径大小的SpriteMask遮罩,在遮罩外部展示的是第一重世界的场景及物体,也就是不使用魔术门下玩家能看见的场景,而在遮罩内部展示的是第二重世界的场景及物体。
三、准备工作
3.1新建2D项目
3.2导入素材包
素材包地址
2D Game Kit | Tutorials | Unity Asset Store
3.3项目目录
3.3.1项目目录
四、正式开始
本次教程使用的是示例工程中的场景三。
4.1场景预览
打开场景三
4.2制作双重世界
由于我们具有一前一后的两个世界,所以需要用到两层场景。
4.2.1世界一
在场景根目录下新建父物体命名为World1,并将LevelAsssets下所有东西放在World1下作为World1的子物体。
4.2.2世界二
拷贝World1父物体并重命名为World2。
如果我们修改World1及World2的z轴坐标,那么二者在场景中的关系应该是如下的。
至此,我们双重世界的基础工作就完成了。
注意:我们无需更改World1与World2的坐标,上述更改只是为了更好演示两个世界逻辑关系,实际中前后关系是通过遮罩实现的。
4.3修改双重世界
我们修改场景三中间移动平台+毒液部分作为示范。我们希望的效果是:当玩家经过此处时,如果开启魔术门,则底下的毒液消失并变为平底,供玩家通过。
4.3.1修改世界一
所以说世界一应当是能被遮罩所遮挡的。为了更直观看见效果,我们先把World2禁用了。由于该项目中的需要被遮罩作用的毒液是采用MeshRenderer渲染的,所以步骤稍微麻烦一点,需要修改shader才能使spriteMask遮罩起作用,如果是一般的sprite渲染的话则相对简单很多。
4.3.1.1找到所需修改的Shader
找到World1中第二个Acid,也就是较长的毒液物体。
找到Acid MeshRenderer中的Water Material,并拷贝两份命名为Water_World1以及Water_World2。
找到Water身上Water2D shader,资源文件夹内搜索Water,找到后如下拷贝两份并命名为Water_World1及Water_World2。
4.3.1.2修改shader
打开编辑Water_World1 shader,主要修改内容如下。
4.3.1.3应用shader
找到Water_World1 Material,将Water_World1 shader应用到该Material上。
4.3.1.4修改Acid毒液
回到场景,关闭Acid身上SpriteMask组件防止干扰,并将上述修改的Water_World1 Material应用在Acid的MeshRenderer上。
4.3.2修改Player
4.3.2.1为Player添加SpriteMask子物体
4.3.2.2修改SpriteMask
遮罩的大小主要是通过SpriteMask的scale的控制。主要需要注意的点已圈出。
4.3.3修改世界一总结
如上修改后,已可实现如下效果。
4.3.4修改世界二
打开World2父物体,由于在World2中玩家应该是可以安全通过下面的毒液的,所以我们讲World2中相同位置上的Acid取消激活。
同理所有我们希望在World2中不显示或者显示的物体,都是通过以上思路实现的
再举例如下:World1与World2是相对的,若希望在World2的某位置生成一个土块,便可在World2下新增物体土块,而World1不新增即可。
4.4编码
由于魔法门是玩家控制开启与关闭的,我们不希望魔法门如刚所示一直呈打开状态。
4.4.1为Player添加脚本MaskController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Gamekit2D;
public class MaskController : MonoBehaviour
{
public GameObject world1; // 世界1父物体
public GameObject world2; // 世界2父物体
public SpriteMask mask; // 遮罩
SpriteRenderer maskSP; // 遮罩sprite
public float maskRadius;// 遮罩半径
[Tooltip("需要被遮罩作用的层")] public LayerMask maskLayers;
[HideInInspector] public List<GameObject> setFalseGameObject; // world1中开启遮罩后取消的所有物体列表
[HideInInspector] public List<GameObject> world1GameObjectList; // world1中所有物体列表
private void Start()
{
maskSP = mask.GetComponent<SpriteRenderer>();
world1GameObjectList = new List<GameObject>();
setFalseGameObject = new List<GameObject>();
InitWorld1GameObject();
}
void Update()
{
MaskOn();
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, maskRadius);
}
/// <summary>
/// 初始化获取World1所有子物体
/// </summary>
void InitWorld1GameObject()
{
int count = world1.transform.childCount;
for (int i = 0; i < count; i++)
{
GetAllChild(world1.transform.GetChild(i).gameObject);
}
}
/// <summary>
/// 获取子物体函数
/// </summary>
/// <param name="father"></param>
void GetAllChild(GameObject father)
{
int count = father.transform.childCount;
for (int i = 0; i < count; i++)
{
world1GameObjectList.Add(father.transform.GetChild(i).gameObject);
}
}
/// <summary>
/// 遮罩控制
/// </summary>
void MaskOn()
{
if (Input.GetKey(KeyCode.Z))
{
mask.enabled = true; // 开启遮罩
maskSP.enabled = true;
world2.gameObject.SetActive(true);
// 在当前位置画等比例大小的检测圆
Collider2D[] collider2Ds = Physics2D.OverlapCircleAll(transform.position, maskRadius, maskLayers);
if (collider2Ds.Length > 0)
{
for (int i = 0; i < collider2Ds.Length; i++)
{
GameObject obj = collider2Ds[i].transform.gameObject;
if (world1GameObjectList.Contains(obj))// 仅作用与world1
{
obj.GetComponent<Collider2D>().enabled = false; // 仅取消碰撞体就可以了
// 下面根据实际情况修改,此处取消Acid的Damage脚本即可。
if (obj.gameObject.GetComponent<Damager>() != null)
{
obj.gameObject.GetComponent<Damager>().enabled = false;
}
setFalseGameObject.Add(obj);
}
}
}
}
if (!Input.GetKey(KeyCode.Z))
{
mask.enabled = false;
maskSP.enabled = false;
world2.gameObject.SetActive(false);
// 恢复消除的obj
for (int i = 0; i < setFalseGameObject.Count; i++)
{
GameObject obj = setFalseGameObject[i].gameObject;
obj.GetComponent<Collider2D>().enabled = true; // 恢复碰撞体
// 对应上面取消的Acid的Damage脚本
if (obj.gameObject.GetComponent<Damager>() != null)
{
obj.gameObject.GetComponent<Damager>().enabled = true;
}
}
setFalseGameObject.Clear();
}
}
}
4.4.2修改Acid
对于World1中Acid,我们是希望Mask能作用于其上的,添加Layer Acid,并修改Acid的Layer。同时,新建父物体命名为Acids,并将Acid设置为Acids的子物体。
4.4.3配置MaskController脚本
4.4.4测试
4.5World2中的拓展
如果说此时我们希望打开魔法门后,能与World2的某个平台交互,而此平台是World1中不存在的,所需修改的地方也很少。
4.5.1整理项目目录
此时项目目录有些许凌乱,我们稍作整理,如下:
即,world1保留所有,不作修改,World2中只保留一些背景以及我们所需的平台PassThroughPlatform。
将平台位置调整为如下:
4.5.2修改平台
如果此时我们运行游戏,会发现平台随着魔法门的开启的出现,但是并没有被遮罩作用,这是因为没有设置平台与遮罩的层级关系。
还是否记得前文提到,MeshRenderer遮罩的实现比较麻烦,而对于Sprite来说,Mask遮罩的实现比较简单,对于这平台来说,就是属于后者情况。
修改平台SpriteRenderer的Mask Interaction属性为visable inside Mask
注意,并非修改平台父物体,而是平台下的子物体。
4.5.3测试
魔术门已对平台产生作用
魔术门关闭时也无法与平台产生交互。
4.6附加
魔术门内外需要提高辨识度。
4.6.1方法:
-
world2中新建square物体,并命名为Mask
-
修改配置Mask
4.6.2测试
五、总结
至此,Unity运用遮罩简单复刻2D平台《Unbound: Worlds Apart》游离于世界之海的双重世界效果已完成。
其他物件的修改基本与上述中毒液以及平台的修改办法一致。
-
对于world1中需要在魔术门内隐藏的物体,采用配置shader的方式,建议毒液的shader及Material尽量复用;
-
对于world2中仅在魔术门内可见的物体,采用配置SpriteRenderer层级的方式,需要和平台一样,配置好world2中所有需要被魔术门作用的物体的层级关系。
同时,原作中在开启魔术门后会有额外的能力,如重力反转等,各位只需在MaskOn函数的按键内补充即可,这里不做重点讲解。
六、最后
以上为本人的一些拙见,皆是亲身尝试过且能实现的,可能原作并不是采用我这种方法来实现的,肯定还有其他的更好的方法能实现更完美的效果。针对上述的方法,可能也有处理不当的地方,大家有建议,补充或者更好的方法也欢迎提出!
同时,欢迎转发,创作不易,需标明出处。