Cocos creator 3.x develops RPG games from scratch (character map materials, joystick control of character movement, and scene obstacle judgment)

I have been lying down recently and haven’t done much writing about games. I have used NovelAi to burn the graphics card and draw girls. I used an RPG game I developed before to write a simple tutorial and short article. The approximate effect is as follows:
Please add an image description
The main contents of this article are as follows: 1. Making characters and map materials 2. Using cocos creator 3.x (this demo uses 3.8) to realize character movement and terrain obstacles

1. Making characters and map materials

This is a very troublesome problem for individual developers (like me). They can’t afford an artist (girl), and they don’t have art talent. They can only piece together materials from the Internet. They also have to pay attention to the fact that domestic copyrights are obtained after all. Come to WeChat Byte Platform…
My personal preferred material website is kenney (https://kenney.nl/assets). I don’t need to explain this. All CC0 materials are commercially available. All the materials I have used are good.
For a game like Aigui, although there are many materials, the materials will appear very inconsistent in a patchwork game. Since we are making an RPG game, I thought of an artifact: RPG Maker!
RPG Maker is an RPG production master. It is an artifact for developing stand-alone RPG games. It contains necessary material production tools such as a map maker and a character generator. We do not necessarily need to be able to use RPG Maker to make games, as long as we can use it. Just generate the map material. Put the generated png image into cocos and use it. This can ensure the coordination of the game material. The integration of characters and map elements will not appear obtrusive. Moreover, RPG Maker also has a wealth of DLC, as well as many foreign The scene created by the master can be used immediately, saving 300% of time and cost! (It should be noted that even if you purchase the genuine RPG Maker, if the materials in it are obtained from other engines such as cocos and unity, it will also be considered infringement and requires Fang Jiaochuan’s authorization. Please pay attention if you are planning to go overseas or upload it to Steam)

This article uses RPG Maker MV. There are a lot of online resources, please go to Baidu. Download, unzip and double-click the exe to run.

After opening the software, create a new project, then right-click on the project in the lower left corner

Right-click to load, and you can see that there are many official demos.
![Insert image description here](https://img-blog.csdnimg. cn/96ba105382554ab986d2489d70f899f9.png

Let’s choose one at random and click OK. This article will take the grotto map as an example (I modified the map in the demo and only kept the upper half. You can try it yourself). After selecting, the software will automatically load the scene. Right-click and save as image. , the map can be saved locally in PNG format, and we can directly put it under cocos and use it as map material. (Pictures can be compressed according to actual needs)

Now that the map is done, let’s start generating characters.
Click the “Character Generator” button in the menu

You can see that this is a very cool generator. “Walking Maps” will be used in the game, and subsequent battle scenes also require “Battle Maps”

Export in PNG format, which is a batch image. We can use tools such as PngSplit to cut the image into independent small images, and then select the materials we need to use and put them in the game directory.

2. Use cocos creator 3.x (this demo uses 3.8) to realize character movement and terrain obstacles

Open cocos creator, create a new project, put the materials generated in the first step into the resource folder, roll up your sleeves and code.

The first is the joystick. There is a demo written by a master on the cocos store. There is no explanation when you use it.

By default, the phone is in landscape orientation, the length and width are set to 1334*750, and the background is enlarged, I put 2000*1200.
It’s roughly this ratio:

The nodes are as follows:

The game is designed so that the player character is always in the middle of the screen, so controlling the joystick operation is equivalent to moving the background in reverse, which means that the character and the joystick are fixed. We use one camera to observe the joystick and the character, and another camera Observe the background.
The hierarchy of the two cameras is as follows:
First camera:

second camera

Set the Layer of the player character and joystick to “Joystick”, and the other nodes default to UI_2D

The Canvas camera is set to the camera observing UI_2D
A new node DefaultManage is added to the scene, which is used to mount scene scripts (code below):

At the same time, configure the attribute node of the joystick:

The target of the joystick is bound in the scene script. What we bind is the background node, not the player node. This is different from the joystick demo.

As expected, the background will follow the movement of the joystick, but the joystick can move diagonally, but our character materials only have four types of up, down, left, and right, so modify the joystick. For example, when the player operates the joystick to 40 degrees, We move the background horizontally instead of diagonally, and only move vertically when the angle is greater than 45 degrees.

Referring to the code of the joystick demo, we add judgment here to ensure that only horizontal or vertical movement is performed.

When the joystick moves, the direction is recorded and the direction the character is facing is displayed.

Regarding obstacles, we use rigid bodies + collision bodies to set up a group to represent areas where movement is prohibited.
Turn on the editing check box of the collision body, and you can move and expand freely. You can lay out all the obstacles in a scene in two or three minutes, which is very efficient.

When the map moves, ray detection is emitted in the direction the player is facing. When the ray detects an obstacle, it is returned. Without moving, the obstacle effect can be achieved.
Why are there two rays used here? When a person walks down, he needs to shoot one on his left hand and one on his right hand. They can walk without collision to prevent half of the body from penetrating obstacles. The same goes for other directions.

The complete code of the node is as follows:

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: "Node where the joystick script is located", tooltip: "Script where the joystick script Joystick is located", type: joy })
    public joy: joy = null!;


    @property({ displayName: "Role (node controlled by joystick)", tooltip: "Role (node controlled by joystick)", type: Node })
    background: Node = null!;


    @property({ displayName: "Whether the character is rotated according to the direction", tooltip: "Whether the character is rotated according to the direction of the joystick" })
    is_angle: boolean = false;


    @property({ displayName: "Whether to imprison the character", tooltip: "Whether to imprison the character. If the character is imprisoned, the character cannot move" })
    is_fbd_background: boolean = false;


    @property({ displayName: "Character movement speed", tooltip: "Character movement speed, not recommended to be too high, 1-10 is best", type: CCFloat })
    speed: number = 3;

    @property({ displayName: "player character", tooltip: "player character", type: Node })
    player: Node = null!;

    // Character's movement vector
    vector: Vec2 = new Vec2(0, 40);

    //Angle of character rotation
    angle: number = 0;

    private lastX: number = 0;
    private direction: string = 'down'
    start() {
    }

    update(deltaTime: number) {
        // If there is no imprisoned character
        if (this.is_fbd_background === false) {
            // Get the character movement vector
            this.vector = this.joy.vector;

            // Vector normalization
            let dir = this.vector.normalize();

            // multiply speed
            let dir_x = dir.x * this.speed;
            let dir_y = dir.y * this.speed;
            //Character coordinates plus direction
            if (Math.abs(dir_x) > Math.abs(dir_y)) {
                //X is greater than Y, indicating that only horizontal movement is performed, and vertical movement is not considered.
                if (dir_x > 0) {
                    //Move the joystick to the right and the map to the left
                    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 & amp; & amp; results_1.length > 0) || (results_2 & amp; & amp; results_2.length > 0)) {
                        return
                    }
                    this.background.setPosition(x, this.background.position.y);

                }
                else if (dir_x < 0) {
                    //Move the joystick to the left and the map to the right
                    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 & amp; & amp; results_1.length > 0) || (results_2 & amp; & amp; results_2.length > 0)) {
                        return
                    }
                    this.background.setPosition(x, this.background.position.y);
                }

            }
            else if (Math.abs(dir_x) < Math.abs(dir_y)) {
                //Only consider vertical orientation
                if (dir_y > 0) {
                    //Move the joystick up and the map moves down
                    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 & amp; & amp; results_1.length > 0) || (results_2 & amp; & amp; results_2.length > 0)) {
                        return
                    }
                    this.background.setPosition(this.background.position.x, y);
                }
                else if (dir_y < 0) {
                    //Move the joystick down and the map moves up
                    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 & amp; & amp; results_1.length > 0) || (results_2 & amp; & amp; results_2.length > 0)) {
                        return
                    }
                    this.background.setPosition(this.background.position.x, y);
                }
            }
            else {
                //If there is no moving joystick, set the player to a non-moving state
                this.playerStop()
            }
        }
    }

    private playerMoving() {
        if (this.direction === 'up' || this.direction === 'down') {
            //The character moves up and down
            let dis = Math.floor(Math.abs(this.background.position.y - 0) / 20);
            if (dis % 2 === 0) {
                resources.load("person/ protagonist 1", SpriteAtlas, (err, atlas) => {
                    this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame(this.direction === 'up' ? 'Protagonist 1_11' : 'Protagonist 1_2') ;
                });
            }
            else {
                resources.load("person/ protagonist 1", SpriteAtlas, (err, atlas) => {
                    this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame(this.direction === 'up' ? 'Protagonist 1_12' : 'Protagonist 1_3') ;
                });
            }
        }
        else {
            //You only need to use the same picture on the left and right to flip the nodes horizontally.
            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/ protagonist 1", SpriteAtlas, (err, atlas) => {
                    this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('Protagonist 1_5');
                });
            }
            else {
                resources.load("person/ protagonist 1", SpriteAtlas, (err, atlas) => {
                    this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('Protagonist 1_6');
                });
            }
        }
    }

    private playerStop() {
        switch (this.direction) {
            case 'up':
                resources.load("person/ protagonist 1", SpriteAtlas, (err, atlas) => {
                    this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('Protagonist 1_10');
                });
                break;
            case 'down':
                resources.load("person/ protagonist 1", SpriteAtlas, (err, atlas) => {
                    this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('Protagonist 1_1');
                });
                break;
            case 'left':
            case 'right':
                this.player.setScale(this.direction === 'right' ? -1 : 1, 1, 1)
                resources.load("person/ protagonist 1", SpriteAtlas, (err, atlas) => {
                    this.player.getChildByName('Sprite').getComponent(Sprite).spriteFrame = atlas.getSpriteFrame('Protagonist 1_4');
                });
                break;
        }
    }
}

It’s twelve o’clock, I’m too sleepy… Please read the code yourself for some details. It’s a very simple demo.

The code and materials are basically all above. If you need the code packaged for this demo, you can also send me a private message. It would be great if you could follow me…

I wish everyone can develop the RPG of their dreams as soon as possible~