最近躺平了,基本没怎么写游戏,挂着NovelAi烧显卡画妹子,把之前开发的一款RPG游戏拿来写个简单的教程,水篇文章,大概的效果如下:

ios unity 退出_摇杆


本文主要内容有以下几点: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运行。

ios unity 退出_RPG_02

打开软件后,新建一个项目,然后右键点击左下角的project

ios unity 退出_RPG_03

右键载入,可以看到有很多官方的demo

ios unity 退出_摇杆_04

我们随便选一个,点击确定,本文就以石窟地图为例(demo中我对该地图做了修改,只保留了上面一半,大家可以自己尝试),选择后软件会自动加载场景,右键保存为图像,就能将该地图以PNG格式保存在本地了,我们直接放到cocos下面作为地图素材使用。(可以根据实际需要对图片进行压缩)

ios unity 退出_RPG_05

地图搞定了,下面开始生成人物。

点击菜单里的“人物生成器”按钮

ios unity 退出_摇杆_06


可以看到这是个非常牛皮的生成器,游戏里会用到“行走图”,后续的战斗场景也需要“战斗图”

ios unity 退出_摇杆_07


ios unity 退出_RPG_08

导出PNG格式,是一个合批图,我们可以使用PngSplit等工具将图片切割成独立的小图,然后挑选需要使用的素材放到游戏目录中。

二、使用cocos creator 3.x(本demo使用3.8)实现人物移动和地形障碍

打开cocos creator,新建个项目,把第一步中生成的素材放到资源文件夹中,撸起袖子码代码。

首先是摇杆,cocos store上有大神写的demo,拿来用不解释。

默认手机横向,长宽设置为1334 * 750,背景放大一些,我放了2000*1200。

大概是这么个比例:

ios unity 退出_RPG_09


节点有以下这些:

ios unity 退出_RPG_10

游戏设计成玩家人物始终在屏幕中间,因此控制摇杆的操作,就等于反向移动背景,也就是说人物和摇杆是固定不动的,我们使用一个相机观察摇杆和人物,另一个相机观察背景。

两个相机的层级如下:

第一个相机:

ios unity 退出_ios unity 退出_11


第二个相机

ios unity 退出_摇杆_12

把玩家人物和摇杆的Layer设置为“摇杆”,其余节点默认为UI_2D

ios unity 退出_ios unity 退出_13

Canvas的相机设置为观察UI_2D的那个相机

ios unity 退出_游戏_14


场景内新增节点DefaultManage,用于挂载场景脚本(代码在后面):

ios unity 退出_游戏引擎_15

同时配置好摇杆的属性节点:

ios unity 退出_ios unity 退出_16

摇杆的目标是在场景脚本里绑定的,我们绑定的是背景节点,并非玩家节点,这个和摇杆demo不一样。

ios unity 退出_ios unity 退出_17

不出意外,背景就会跟随摇杆移动了,但是摇杆可以斜着移动,但我们的人物素材只有上下左右四种,所以把摇杆修改一下,比如玩家把摇杆操作为40度时,我们让背景水平移动,而非斜着移动,当角度大于45度时,只进行垂直移动。

ios unity 退出_RPG_18


参考摇杆demo的代码,我们在这增加判断,保证只进行水平或垂直移动。

ios unity 退出_游戏_19


摇杆移动时,记录方向,显示人物面对的方向。关于障碍物,我们使用刚体+碰撞体,设置一个分组,用于表示禁止移动的地区。(

打开碰撞体的editing勾选框,可以自由移动和伸缩,两三分钟就能把一个场景内的全部障碍物铺完,非常效率。(demo里使用了很多个盒碰撞,实际建议用多边形碰撞组件PolygonCollider2D,可以减少节点个数,也方便查找和维护。另外这玩意在微信小游戏里非常吃内存,如果一个场景有很多刚体会直接卡死,解决方案有两个:1不用碰撞体,用普通节点,自己写碰撞逻辑(结合移动速度预测下一帧的人物位置是否和其他节点有碰撞);2在微信MP开启高性能模式,解决卡顿问题,但发热严重。)

ios unity 退出_摇杆_20


地图移动时,在玩家面对的方向发射射线检测,当射线检测到障碍物时return掉,不做移动,即可达到障碍效果。

这里为什么用两条射线呢,人往下走时,需要在左手和右手各发射一条,都没有碰撞才能走,防止一半身子穿透障碍物的情况,其他方向同理。

ios unity 退出_游戏引擎_21


节点的完整代码如下:

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小游戏平台的《星咏骑士》体验