Tilemap from spritesheet


I want to generate room layouts in my game by using a sprite sheet, because I am not normal, so I started creating my tilemap project. It can be found on this link: Tilemap


The result of this is a simple, floor, map on a single plane mesh that is created dynamically from 64x40 smaller planes and UV has been stretched (size depends on the size of the PNG and size of the single cell tile). Here is the result of the tilemap:


It is harder to see but it’s 1 draw call to apply everything. I am kind off stuck at how I would be able to make a plugin to draw in editor since I have never created any plugins but I would like to learn that if anyone can point me in any direction.


For now, this only works for this exact PNG since there are a lot of magic numbers (manually written UV scale, rows/cols, tile position, etc.). Will be changed in due time.

1 Like

There are couple of ways this could be done:

  • Duplicate the rendering code in a ‘User Script’ (not ideal) Editor API | Learn PlayCanvas (this is what I did for Loading GLBs in Editor Viewer myself)

  • Use the Editor API to load the script onto the entity and call the functions needed to render/add the tilemap (not tried this myself so actually a little interested in this one)

  • Use @Leonidas 3d party tools https://github.com/leonidaspir/uranus-editor-scripting to script from the Editor (Edit: Oh, it’s deprecated. Not sure what the current state is :sweat_smile: )

2 Likes

That all sounds so complicated, especially since I don’t understand how it works. Probably since I have never done any of it before. Will mess around with first option to understand what is happening and how some of the things work.

If it helps, you can see an example I made to load GLBs yaustar.github.io/playcanvas-editor-api-tools at master · yaustar/yaustar.github.io · GitHub

I might give a try myself on the 2 option as I’ve not done that before!

1 Like

The coding looks similar to creating a custom editor script in Unity (that I have done before). Will check it out, thank you!

I’ve got some partial success here with my LDTK project:

Will tidy it up and share the code in a bit

1 Like

Wow, you are fast. I am having trouble with userscript. Why does this work:

app.mouse.on('mousedown', (e) => {
    console.log('Hello');
});

but this doesn’t:

app.keyboard.on('keydown', (e) => {
    console.log('Hello');
});

It’s probably because the keyboard controller isn’t attached to the engine in the editor view

Here’s my user script that is designed specifically for my LDTK project linked https://playcanvas.com/project/762870 :slight_smile:

// ==UserScript==
// @name        LDTK loading test - playcanvas.com
// @namespace   Violentmonkey Scripts
// @match       https://playcanvas.com/editor/scene/1086405
// @grant       none
// @version     1.0
// @author      -
// @description 28/07/2022, 12:16:55
// ==/UserScript==

(function () {
    'use strict';
    const logCssStyle = 'color: white; background-color: #8e44ad';
    const onEngineLoaded = function () {
        console.log("%c ldtk loaded :) ", logCssStyle);

        const app = pc.Application.getApplication();
        let menu = null;
        const root = editor.call('layout.root');

        let mousedownX = 0;
        let mousedownY = 0;
        let mousedownTimeStamp = 0;

        const loadAsset = function (assetId, callback) {
            const asset = app.assets.get(assetId);
            if (asset) {
                asset.ready((a) => {
                    console.log(a.name + ' loaded');
                    if (callback) {
                        callback();
                    }
                });
                app.assets.load(asset);
            }
        };

        app.mouse.on('mousedown', (e) => {
            const oldX = mousedownX;
            const oldY = mousedownY;
            const oldTimeStamp = mousedownTimeStamp;

            mousedownX = e.x;
            mousedownY = e.y;
            mousedownTimeStamp = Date.now();

            // Check for double click option on right mouse so we can
            // walk up the parents as quick shortcut
            if (e.button === pc.MOUSEBUTTON_RIGHT) {
                if (Math.abs(oldX - e.x) < 5 || Math.abs(oldY - e.y) < 5) {
                    if (Date.now() - oldTimeStamp < 250) {
                        const items = editor.selection.items;
                        if (items.length === 1 && items[0] instanceof api.Entity) {
                            const parent = items[0].parent;
                            if (parent) {
                                editor.selection.set([parent]);

                                // Don't have the context menu show up
                                mousedownX = Number.MAX_VALUE;
                                mousedownY = Number.MAX_VALUE;
                            }
                        }
                    }
                }
            }
        });

        app.mouse.on('mouseup', (e) => {
            // Did we move the mouse while holding mouse button
            if (Math.abs(mousedownX - e.x) > 5 || Math.abs(mousedownY - e.y) > 5) {
                return;
            }

            if (e.button === pc.MOUSEBUTTON_RIGHT) {
                const items = editor.selection.items;
                const menuItems = [];

                if (menu) {
                    root.remove(menu);
                    menu = null;
                }

                if (items.length === 1 && items[0] instanceof api.Entity) {
                    const selectedEntity = items[0];
                    const scripts = selectedEntity.get('components.script.scripts');
                    const guid = selectedEntity.get('resource_id');
                    const viewEntity = app.root.findByGuid(guid);

                    if (scripts && scripts.main && (!viewEntity.script || !viewEntity.script.main)) {
                        menuItems.push({
                            text: 'Load LDTK',
                            onSelect: () => {
                                const attributes = scripts.main.attributes;
                                let assetsLoaded = 0;
                                let totalAssets = 0;

                                const assetLoadCallback = () => {
                                    ++assetsLoaded;
                                    if (assetsLoaded === totalAssets) {
                                        viewEntity.addComponent('script');
                                        viewEntity.script.create('main', scripts.main);
                                        console.log('Done');
                                    }
                                }

                                // Load the JSON config
                                loadAsset(attributes.ldtkAsset, assetLoadCallback);
                                ++totalAssets;

                                // Load the layers textures
                                const layersConfigs = attributes.layersConfig;
                                for (let i = 0; i < layersConfigs.length; i++) {
                                    loadAsset(layersConfigs[i].tileSheetAsset, assetLoadCallback);
                                    ++totalAssets;
                                }

                                // Load the script in the engine
                                const scriptAsset = editor.assets.filter((a) => { return a.get('name') === 'main.js' && a.get('type') === 'script' });
                                loadAsset(scriptAsset[0].get('id'), assetLoadCallback);
                                ++totalAssets;
                            }
                        });
                    }
                }

                if (menuItems.length > 0) {
                    const menuArgs = {
                        items: menuItems,
                    };

                    menu = new pcui.Menu(menuArgs);
                    root.append(menu);

                    // Do this on the next frame to work
                    setTimeout(function () {
                        menu.hidden = false;
                        menu.position(e.event.clientX + 1, e.event.clientY);
                    });
                }

                e.event.preventDefault();
                e.event.stopPropagation();
            }
        });
    };

    // Wait for the PlayCanvas application to be loaded
    const intervalId = setInterval(function () {
        if (pc.Application.getApplication()) {
            onEngineLoaded();
            clearInterval(intervalId);
        }
    }, 500);
})();

1 Like

That’s actually pretty cool :smiley: ! When I understand UserScript better I might mess around with it more! Thanks for helping me out.

Made the keyboard work in userscript, wooo :smiley: !! This is so much fun. Also, I am wondering if I can manipulate everything with this userscript.

1 Like

This is what I would like to make to be able to use in editor! This would be useful for me…might as well try to make with userscripting (and…almost 0 knowledge).


This is now in JS just to see how it could work. Basically, I want to make a single plane from a PNG spritesheet by drawing it in the editor.

Hey, it has been a very long time since I did something on PlayCanvas. Got extremly busy with three jobs so had no time to develop anything I started. Luckily, got some free time and thought to myself ‘Why have fun in real life when I can enjoy working more’. I can tell you that my girlfriend didn’t like that statement.


So, I started messing with editor tools a bit, to remember how things work, and I am so happy that I came back to do more fun things. There are currently two things I am doing, Spritesheet Editor and CameraSystem, but I am doing them very slowly since I am very busy.


Here are my PlayCanvas editor changes. My OCD made me make it look how I like!

1 Like

Oh, that’s cool! You’ve added a global menu bar for your own menu items

1 Like

@yaustar Yeah! The script is weird but it works! Hehe. Also removed ‘Chat’ and ‘ActiveIcon’ since I am mostly doing things alone so it was bugging me!