最近躺平了,基本没怎么写游戏,挂着NovelAi烧显卡画妹子,把之前开发的一款RPG游戏拿来写个简单的教程,水篇文章,大概的效果如下:
本文主要内容有以下几点:1.制作人物及地图素材 2.使用cocos creator 3.x(本demo使用3.8)实现人物移动和地形障碍
一、制作人物及地图素材
这个对于个人开发者而言(比如我)是个非常头痛的难题,请不起美工(妹子),自己又没有美术天赋,只能从网上找素材东拼西凑,还要留意国内版权毕竟是拿来上微信字节平台的…
个人首选的素材网是kenney(https://kenney.nl/assets)这个不解释,全CC0可商用素材,用过的都说好。
像爱给这种的,虽然素材多,但是七拼八凑做出来的游戏,素材会显得非常不协调,既然是做RPG游戏,我想到一款神器:RPG Maker!
RPG Maker即RPG制作大师,是款开发单机RPG游戏的神器,里面包含了地图制作器、人物生成器等必要的素材制作工具,我们不一定要会用RPG Maker来做游戏,只要会拿它来生成地图素材就行了,生成的png图片放到cocos里用,这样就能保证游戏素材的协调性,人物和地图元素融为一体不会显得突兀,而且RPG Maker还有丰富的DLC,以及很多国外大神制作好的场景,拿来即用,时间和成本立省300%!(需要注意的是,即使是购买了正版RPG Maker,里面的素材如果拿到其他引擎比如cocos、unity,也属于侵权,需要获得方角川授权,准备出海或者上steam的需要留意下)
本文使用RPG Maker MV,网上资源一大堆请自行百度,下载解压后双击exe运行。
打开软件后,新建一个项目,然后右键点击左下角的project
右键载入,可以看到有很多官方的demo
我们随便选一个,点击确定,本文就以石窟地图为例(demo中我对该地图做了修改,只保留了上面一半,大家可以自己尝试),选择后软件会自动加载场景,右键保存为图像,就能将该地图以PNG格式保存在本地了,我们直接放到cocos下面作为地图素材使用。(可以根据实际需要对图片进行压缩)
地图搞定了,下面开始生成人物。
点击菜单里的“人物生成器”按钮
可以看到这是个非常牛皮的生成器,游戏里会用到“行走图”,后续的战斗场景也需要“战斗图”
导出PNG格式,是一个合批图,我们可以使用PngSplit等工具将图片切割成独立的小图,然后挑选需要使用的素材放到游戏目录中。
二、使用cocos creator 3.x(本demo使用3.8)实现人物移动和地形障碍
打开cocos creator,新建个项目,把第一步中生成的素材放到资源文件夹中,撸起袖子码代码。
首先是摇杆,cocos store上有大神写的demo,拿来用不解释。
默认手机横向,长宽设置为1334 * 750,背景放大一些,我放了2000*1200。
大概是这么个比例:
节点有以下这些:
游戏设计成玩家人物始终在屏幕中间,因此控制摇杆的操作,就等于反向移动背景,也就是说人物和摇杆是固定不动的,我们使用一个相机观察摇杆和人物,另一个相机观察背景。
两个相机的层级如下:
第一个相机:
第二个相机
把玩家人物和摇杆的Layer设置为“摇杆”,其余节点默认为UI_2D
Canvas的相机设置为观察UI_2D的那个相机
场景内新增节点DefaultManage,用于挂载场景脚本(代码在后面):
同时配置好摇杆的属性节点:
摇杆的目标是在场景脚本里绑定的,我们绑定的是背景节点,并非玩家节点,这个和摇杆demo不一样。
不出意外,背景就会跟随摇杆移动了,但是摇杆可以斜着移动,但我们的人物素材只有上下左右四种,所以把摇杆修改一下,比如玩家把摇杆操作为40度时,我们让背景水平移动,而非斜着移动,当角度大于45度时,只进行垂直移动。
参考摇杆demo的代码,我们在这增加判断,保证只进行水平或垂直移动。
摇杆移动时,记录方向,显示人物面对的方向。关于障碍物,我们使用刚体+碰撞体,设置一个分组,用于表示禁止移动的地区。(
打开碰撞体的editing勾选框,可以自由移动和伸缩,两三分钟就能把一个场景内的全部障碍物铺完,非常效率。(demo里使用了很多个盒碰撞,实际建议用多边形碰撞组件PolygonCollider2D,可以减少节点个数,也方便查找和维护。另外这玩意在微信小游戏里非常吃内存,如果一个场景有很多刚体会直接卡死,解决方案有两个:1不用碰撞体,用普通节点,自己写碰撞逻辑(结合移动速度预测下一帧的人物位置是否和其他节点有碰撞);2在微信MP开启高性能模式,解决卡顿问题,但发热严重。)
地图移动时,在玩家面对的方向发射射线检测,当射线检测到障碍物时return掉,不做移动,即可达到障碍效果。
这里为什么用两条射线呢,人往下走时,需要在左手和右手各发射一条,都没有碰撞才能走,防止一半身子穿透障碍物的情况,其他方向同理。
节点的完整代码如下:
import { _decorator, CCFloat, Component, ERaycast2DType, Node, PhysicsSystem2D, Prefab, resources, Sprite, SpriteAtlas, Vec2, Vec3 } from 'cc';
import joy from "./Joystick"
const { ccclass, property } = _decorator;
@ccclass('DefaultManage')
export class DefaultManage extends Component {
@property({ displayName: "摇杆脚本所在节点", tooltip: "摇杆脚本Joystick所在脚本", type: joy })
public joy: joy = null!;
@property({ displayName: "角色(受摇杆控制的节点)", tooltip: "角色(受摇杆控制的节点)", type: Node })
background: Node = null!;
@property({ displayName: "是否根据方向旋转角色", tooltip: "角色是否根据摇杆的方向旋转" })
is_angle: boolean = false;
@property({ displayName: "是否禁锢角色", tooltip: "是否禁锢角色,如果角色被禁锢,角色就动不了了" })
is_fbd_background: boolean = false;
@property({ displayName: "角色移动速度", tooltip: "角色移动速度,不建议太大,1-10最好", type: CCFloat })
speed: number = 3;
@property({ displayName: "玩家人物", tooltip: "玩家人物", type: Node })
player: Node = null!;
// 角色的移动向量
vector: Vec2 = new Vec2(0, 40);
// 角色旋转的角度
angle: number = 0;
private lastX: number = 0;
private direction: string = 'down'
start() {
}
update(deltaTime: number) {
// 如果没有禁锢角色
if (this.is_fbd_background === false) {
// 获取角色移动向量
this.vector = this.joy.vector;
// 向量归一化
let dir = this.vector.normalize();
// 乘速度
let dir_x = dir.x * this.speed;
let dir_y = dir.y * this.speed;
// 角色坐标加上方向
if (Math.abs(dir_x) > Math.abs(dir_y)) {
//X大于Y,表示只进行横向移动,不考虑纵向
if (dir_x > 0) {
//摇杆往右移动,地图往左移
let x = this.background.position.x - dir_x;
this.direction = 'right'
this.playerMoving()
const results_1 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x + 40, this.player.getWorldPosition().y), ERaycast2DType.Any);
const results_2 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x + 40, this.player.getWorldPosition().y - 60), ERaycast2DType.Any);
if ((results_1 && results_1.length > 0) || (results_2 && results_2.length > 0)) {
return
}
this.background.setPosition(x, this.background.position.y);
}
else if (dir_x < 0) {
//摇杆往左移动,地图往右移
let x = this.background.position.x - dir_x;
this.direction = 'left'
this.playerMoving()
const results_1 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x - 40, this.player.getWorldPosition().y), ERaycast2DType.Any);
const results_2 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x - 40, this.player.getWorldPosition().y - 60), ERaycast2DType.Any);
if ((results_1 && results_1.length > 0) || (results_2 && results_2.length > 0)) {
return
}
this.background.setPosition(x, this.background.position.y);
}
}
else if (Math.abs(dir_x) < Math.abs(dir_y)) {
//只考虑纵向
if (dir_y > 0) {
//摇杆往上移动,地图往下移
let y = this.background.position.y - dir_y;
this.direction = 'up'
this.playerMoving()
const results_1 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x + 30, this.player.getWorldPosition().y + 5), ERaycast2DType.Any);
const results_2 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x - 30, this.player.getWorldPosition().y + 5), ERaycast2DType.Any);
if ((results_1 && results_1.length > 0) || (results_2 && results_2.length > 0)) {
return
}
this.background.setPosition(this.background.position.x, y);
}
else if (dir_y < 0) {
//摇杆往下移动,地图往上移
let y = this.background.position.y - dir_y;
this.direction = 'down'
this.playerMoving()
const results_1 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x + 30, this.player.getWorldPosition().y - 65), ERaycast2DType.Any);
const results_2 = PhysicsSystem2D.instance.raycast(this.player.getWorldPosition(), new Vec3(this.player.getWorldPosition().x - 30, this.player.getWorldPosition().y - 65), ERaycast2DType.Any);
if ((results_1 && results_1.length > 0) || (results_2 && results_2.length > 0)) {
return
}
this.background.setPosition(this.background.position.x, y);
}
}
else {
//没有移动摇杆,设置玩家为非移动状态
this.playerStop()
}
}
}
private playerMoving() {
if (this.direction === 'up' || this.direction === 'down') {
//角色进行上下移动
let dis = Math.floor(Math.abs(this.background.position.y - 0) / 20);
if (dis % 2 === 0) {
resources.load("person/主角1", SpriteAtlas, (err, atlas) => {
this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame(this.direction === 'up' ? '主角1_11' : '主角1_2');
});
}
else {
resources.load("person/主角1", SpriteAtlas, (err, atlas) => {
this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame(this.direction === 'up' ? '主角1_12' : '主角1_3');
});
}
}
else {
//左右只需要用同一张图,进行节点的水平翻转即可
this.player.setScale(this.direction === 'right' ? -1 : 1, 1, 1)
let dis = Math.floor(Math.abs(this.background.position.x - 0) / 20);
if (dis % 2 === 0) {
resources.load("person/主角1", SpriteAtlas, (err, atlas) => {
this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('主角1_5');
});
}
else {
resources.load("person/主角1", SpriteAtlas, (err, atlas) => {
this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('主角1_6');
});
}
}
}
private playerStop() {
switch (this.direction) {
case 'up':
resources.load("person/主角1", SpriteAtlas, (err, atlas) => {
this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('主角1_10');
});
break;
case 'down':
resources.load("person/主角1", SpriteAtlas, (err, atlas) => {
this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('主角1_1');
});
break;
case 'left':
case 'right':
this.player.setScale(this.direction === 'right' ? -1 : 1, 1, 1)
resources.load("person/主角1", SpriteAtlas, (err, atlas) => {
this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('主角1_4');
});
break;
}
}
}
十二点了,太困了…还有一些细节请自己阅读代码吧,是个非常简单的demo。
代码和素材基本都在上面了,需要此demo打包的代码也可以私信我,如果能点个关注那就太棒了…
祝大家早日开发出自己梦想中的那款RPG~
如果对游戏内容感兴趣,可搜索微信、抖音、QQ小游戏平台的《星咏骑士》体验