如何在游戏里当好一个反派——用Unity简单复刻《勇者别嚣张》(上)

作者:沈琰
本篇难度:★☆☆☆☆
前言
这期准备复刻一个曾经沉迷好久的PSP上的小游戏:《勇者别嚣张》(勇者のくせになまいきだ)。
不得不说一下,这个游戏名的翻译真的是很出彩,完美体现了游戏诙谐幽默的风格。
首先简单介绍一下游戏玩法:
游戏一反玩家代表正义的传统套路,扮演一个破坏神指引大魔王攻占地表。
大魔王一开始蜷缩在地下并且没有任何战斗力,所能依赖的只有一把锄头。敲开地牢内的砖块能根据砖块吸收的养分生成不同类型的魔物。

魔物并不受大魔王的控制,而是有着自己的行为逻辑与生息规则。除此之外当发现勇者时会主动展开进攻。
游戏内每间隔一段时间会有勇者来进攻,如果让勇者抓到大魔王并带出地牢,游戏就失败了。反之如果能杀光每一波来犯的勇者,坚持一定波数后大魔王就能占领地表。

游戏的类型是策略类,核心玩法也是最为有趣的地方是通过敲砖块构成一个合理的生态链,来抵御勇者的攻击。

接下来就用相对简单的方法尝试在Unity中去复刻这个游戏的基本玩法,那么就不多说了,开始干活。
(另:就用这篇文章祭奠我那成天上课不好好听讲,趴在最后一排玩从同桌那借来的PSP的高中生活....)
素材获取
这个步骤本来没打算写,但是想到可能会有同学在复刻小项目时受限于资源问题,加之这次寻找素材的经历有点纠结,就顺带简单提一下,权当给大家一个参考。
既然是复刻一般是先去找有没原版素材,这种游戏要是去扣图怕是得累死。从常见的素材网站找了一圈最后一无所获,只能把打主意在解包上面。
之前没有接触过PSP游戏的解包加上年代久远,寻找办法着实费了一番功夫,最后在一个PSP汉化的帖子里得到方法。
简而言之就是:
1.能被PSP模拟器识别的.iso格式文件可以直接修改扩展名为.zip进行解压缩。
2.图片类的素材文件一般保存在解压缩后的"PSP_GAME\USRDIR\data\graph""路径中。
3.PSP游戏内的图片格式是.gim,还需要专门的gim->png转换工具转换一道。
经过以上三个步骤后,得到了大部分的图片素材,但唯独缺少了勇者和怪物的动画素材。我之前认为这种像素游戏的动画可能也是以帧动画的形式逐帧保存为图片文件的,但翻遍了整个解包出来的文件夹也找不到,最后目光落在了graph文件夹内的一个文件上:

看着这个可疑的名字和大小,我猜测很大几率这就是我要找的东西,但是使尽浑身解数也没弄明白这个.fbe的格式究竟是个啥,无奈之下只能另找别的素材来代替原版的角色动画。希望知道的大佬能在评论区留言以解我心头之惑。
最后再多嘴一句,原则上来说不应该发布解包的过程和素材,无论如何也算是侵权的行为,以往文章中的复刻小项目尽量都是通过别的方法绕开的素材限制。
但这次的游戏想要自制或是替换素材都很麻烦,也很难体现原版的玩法和风格,所以大部分还是使用的原版素材,希望感兴趣的同学们仅作学习之用,不要滥用。
地图搭建
总而言之素材基本准备齐全了,开始动工。

首先开始搭建地图,原版游戏里的场景里除了上面的背景,其他都是网格状的结构。这应该是最适合使用TileMap组件的类型了,所以千万别傻傻的手动去搭地图了。
关于TileMap的使用,专栏里有一期文章专门介绍过,不熟悉的同学可以先去看看。
从原版来看游戏的场景地图分两种:背景图和能敲碎的砖块。关于背景图搭建就不一步步的细说了,记得有些连在一起的地面图片先切分一下,然后参考上面那篇文章,最后搭出来大概是这个样子:

这里主要简单说下能敲碎的砖块的处理。
砖块就没办法光用TileMap组件本身就能实现,因为要处理砖块敲开的逻辑并且每一个砖块都会根据所吸收的养分显示不同的样子,所以每一个砖块是独立的预制体,也就是说不能把所有砖块精灵图片纳入一个tilemap内。那是不是意味着砖块只能靠手搭?并不是。
TileMap不止是能把精灵图片按网格大小均匀排列的,同样的逻辑也能用在预制体上,这一点在官方TileMap教程视频上有展示过。

这个扩展Brush(笔刷)集成在官方的2d-extras(https://github.com/Unity-Technologies/2d-extras )包内,下载导入GitHub上的工程包后,在Project面板中就能新建Prefab Brush,命名后拖入预制体就能使用。

然后选取刚创建的预制体笔刷,指定想要画出的预制体。如果你添加了不止一个预制体,它会在这些预制体中随机选取进行绘制。

交互逻辑
在原版游戏中唯一能做的事就是敲砖块生成道路和魔物(其实还能直接敲死魔物和迷宫中随机生成的宝箱,先暂时不考虑),接下来就是实现这个功能。
第一步是检测到鼠标点击的砖块,这里开始遇到一个小问题:砖块显示在场景内是用的Sprite Renderer组件。它无法像UI下的Image组件一样响应鼠标点击事件,如果把显示组件换成Image也感觉怪怪的,因为砖块是属于场景中的物体,跟UI并没有什么关系。
那么备选方案还有射线检测,从鼠标点击的位置打一条射线到场景中。不过同样有个问题,3D的射线无法检测到2D的碰撞盒,所以砖块上要挂上3D的Box Collider组件。作为一个2D游戏来说虽然可行,但同样显得怪怪的。
所以以上方法通通PASS,用个稍微绕一些的方法。(原因以后会说)
把整张地图看做一个二维数组,那么地图上的砖块都有一个唯一的整数二维坐标。所要做的就是在地图初始化的时候把每个砖块在场景中的坐标转换成二维数组坐标存入字典里。
在鼠标点击时获取鼠标当前的屏幕坐标转换到世界坐标再转成二维数组坐标,用这个二维坐标去字典中获取砖块。
这时候就需要自己定义一个结构来保存二维数组坐标,顺便写上转换函数:
public struct Pos:IEquatable<Pos>
{
public int x;
public int y;
public Pos(int x, int y)
{
this.x = x;
this.y = y;
}
public override string ToString()
{
return "X:" + x + "|" + "Y:" + y;
}
public static Pos Float2IntPos(Vector2 pos)
{
int x = Mathf.FloorToInt((pos.x + 0.09f) / 0.18f);
int y = Mathf.FloorToInt((pos.y + 0.09f )/ 0.18f);
return new Pos(x, y);
}
public static Vector2 Pos2Vector2(Pos pos)
{
float x = pos.x * 0.18f ;
float y = pos.y * 0.18f ;
return new Vector2(x, y);
}
public bool Equals(Pos p)
{
if (this.x == p.x && this.y == p.y)
{
return true;
}
return false;
}
}
为什么从int转float是乘以0.18f?因为图片里一个block的大小就是18*18:

这样一来就把问题转换了一下,通过在字典里匹配鼠标转换坐标的方式解决了这个问题。
还有一个需要注意的地方,按原版游戏逻辑,能够消除的砖块必须是四面至少有一个缺口的,不然把大魔王方到地牢中央的死路中勇者就只能抓瞎了....
这一步就很简单了,用2D射线检测或是2D相交球检测,在执行点击操作之前判断一下周围邻居的个数。
public bool IsCanBroke
{
get
{
return !CheckNei***or(Vector2.up) || !CheckNei***or(Vector2.down) || !CheckNei***or(Vector2.left) || !CheckNei***or(Vector2.right);
}
}
bool CheckNei***or(Vector2 dir)
{
RaycastHit2D [] hit;
hit = Physics2D.RaycastAll(transform.position, dir, 0.18f,1<<LayerMask.NameToLayer("Block"));
return hit.Length>1;
}
接下来编辑砖块的脚本,让砖块显示的的图片根据当前所储存的养料变化。
public Sprite[] sprites;
SpriteRenderer Renderer;
int nutrient = 0;
private void Awake()
{
Renderer = GetComponent<SpriteRenderer>();
nutrient = Random.Range(-9, 15);
}
void Start ()
{
SpriteUpdate();
}
void SpriteUpdate()
{
//change by nutrient
int n=0;
if(nutrient<=-50)
{
MonsterIndex = 5;
n = 6;
}
else if(nutrient<=-30)
{
MonsterIndex = 4;
n = 5;
}
else if(nutrient<=-10)
{
MonsterIndex = 3;
n = 4;
}
else if(nutrient<=10)
{
MonsterIndex = -1;
n = 3;
}
else if(nutrient<=30)
{
MonsterIndex = 2;
n = 2;
}
else if(nutrient<=50)
{
MonsterIndex = 1;
n = 1;
}
else if(n>50)
{
MonsterIndex = 0;
n = 0;
}
Renderer.sprite = sprites[2 * n + Random.Range(0, 2)];
}
public bool OnClick()
{
if(IsCanBroke)
{
if(MonsterIndex>=0)
{
Debug.Log("on click");
}
Destroy(gameObject);
return true;
}
else
{ Debug.Log("cant broke");
return false;
}
}
public void ChangeNutrient(int vaule)
{
nutrient += vaule;
SpriteUpdate();
}
接下来试一试效果:

结束
这期应该只算是把准备工作基本做完了,可以看到代码方面基本没什么难度,主要是熟悉TileMap组件的使用,即便新手也能轻松做出来。顺便说一句,TileMap能做到的事远不止如此,对画笔的自定义扩展能极大的提升开发2D游戏的效率,有兴趣的同学可以自行查阅。
下期内容主要是游戏核心玩法逻辑的实现,包括怪物的状态机和勇者的寻路AI等等。
本期工程地址:https://github.com/tank1018702/unity-005
最后想系统学习游戏开发的童鞋,欢迎访问 http://levelpp.com/
游戏开发搅基QQ群:869551769
微信公众号:皮皮关