Melon-Solid, a JSX approach for Melon.js

Melon.js adapters for Solid.js

Melon-Solid on GitHub
Editable Demo
Why is this useful? I believe Solid gives Melon a way to improve the experience for developers and users for a couple reasons. Melon-Solid is still young, but I believe for it to reach it’s maximum potential it will need the co-operation of the Melon.js community.

#1: A more visual representation

Here’s the normal way we’d mount our Melon.js app

function onload() {
    if (!me.video.init(800, 600, {
      parent: 'screen',
      scaleMethod: 'flex-width',
      renderer: me.video.AUTO,
      preferWebGL1: false,
      subPixel: false
    })) {
        alert('Your browser does not support HTML5 canvas.')
        return
    }
    
    me.audio.init('mp3,ogg')
    me.loader.preload(resources, () => {
        me.state.set(me.state.PLAY, new PlayScreen())
        me.state.transition('fade', '#FFFFFF', 250)
        me.pool.register('mainPlayer', PlayerEntity)
        me.pool.register('SlimeEntity', SlimeEnemyEntity)
        me.pool.register('FlyEntity', FlyEnemyEntity)
        me.pool.register('CoinEntity', CoinEntity, true)
    }
    me.state.change(me.state.PLAY)
}

Now here’s the same code written with Melon-Solid

function App(){
    <Melon audio>
        <Preloader callback={()=>{
            me.state.transition('fade', '#FFFFFF', 250)
            game.texture = new me.TextureAtlas(me.loader.getJSON('texture'), me.loader.getImage('texture'))
        }} resources={resources} autoPlay>
            <Stage state={me.state.PLAY} stage={new PlayScreen()}/>
            <Entity name="FlyEntity" class={FlyEnemyEntity}/>
            <Entity name="SlimeEntity" class={SlimeEnemyEntity}/>
            <Entity name="CoinEntity" class={CoinEntity}/>
            <Entity name="mainPlayer" class={PlayerEntity}/>
        </Preloader>
    </Melon>
}

I’d like to make the case that this is a much better visual representation of whats going on. We don’t need to call a bunch of methods on the Melon.js API. We simply make a JSX version of the content of our app, and Melon-Solid handles the rest. The game will automatically be mounted when our App component is added to the page (to see more about this see the Solid.js docs)

#2: Reactive HTML & Framework Integration

Normally integrating HTML with Melon.js is a little bit difficult. We have to mount Melon.js to a custom component, then manually append our HTML onto it. With JSX (basically like HTML in JS) all of that is handled for us. If we want to add some HTML on top of our game, we can simply put it inside our component.

function App() {
    return (
        <Melon scaleMethod="fit-max" width={880} height={1250}>
            <Preloader callback={()=>{ 
                me.state.transition('fade', '#000000', 250)
            }} resources={resources} autoPlay>
                <Stage state={me.state.PLAY} stage={new PlayScreen()}/>
                <div style={{'font-size': '10em', color: 'white', position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)'}}>Castles are Cool!</div>
            </Preloader>
        </Melon>
    )
}

Now our text renders directly on top of our game. Also using Solid.JS, we can use it’s built in state management (called Signals) to add or remove HTML content from the page reactively. Here we show a message about how much we love castles only after the user presses a button admitting that they too love castles.

import {createSignal} from 'solid-js'
function App() {
    const [isActive, setActive] = createSignal(false)
    return (
        <Melon scaleMethod="fit-max" width={880} height={1250}>
            <Preloader callback={()=>{ 
                me.state.transition('fade', '#000000', 250)
            }} resources={resources} autoPlay>
                <Stage state={me.state.PLAY} stage={new PlayScreen()}/>
                <button style={{'font-size': '3em'}} onClick={()=>{setActive(true)}}>Click if you like castles</button>
                {isActive() ? <div style={{'font-size': '10em', color: 'white', position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)'}}>Castles are Cool!</div> : null}
            </Preloader>
        </Melon>
    )
}

This topic gets a bit complex so I won’t go in depth here, but because we are now building our apps inside of Solid, we can take advantage of all the cool frameworks that can be used to build Solid applications. This includes Vite (used in the demo), and Astro. If you don’t want to use one of these you can still use Solid on it’s own but I’d recommend picking them up. Either way, Solid.js is a great tool for creating dynamic apps that combine Melon.js and HTML.

This also opens up another awesome potential feature: Isomorphic Preloading
Because our resources are in JSX, we can use the Isomorphic capabilities of Astro/Vite to prefetch our those resources in our initial page HTML. This means we can load our game assets before our JS even loads, so when our game does load our assets are most likely instantly available.

#3: Component & Page Lifecycle for better loading behavior

Solid.js provides us with the perfect tools for loading and unloading our games and levels. Because we are making our game into JSX components, Melon.js can automatically know when to load and teardown our code using the onMount and onCleanup hooks (see here).

Here’s the main issues that I ran into:

Can’t have multiple games at the same time

Everything in Melon.js is global, and it can’t logically separate multiple games.
This issue compounds when we use client side routing. Client side routing is when we use JS to automatically handle navigation for us. This means that the browser never actually refreshes. Because of this when we navigate to another page which has another game, all the code from the last game is still running at the same time as all the code from our current game (just like if we have multiple games on the same page)

Everything uses classes

Classes are cool, but with JSX I don’t think they would be necessary. Currently we still need to supply our classes inside our JSX

<Stage state={me.state.PLAY} stage={new PlayScreen()}/>
<Entity name="FlyEntity" class={FlyEnemyEntity}/>

JSX could enable us to make Stages and Entities without needing classes at all.
Here’s what PlayScreen currently looks like (from the platformer example):

class PlayScreen extends me.Stage {
  /**
   *  action to perform on state change
   */
  onResetEvent() {
    // load a level
    me.level.load('map1')

    // reset the score
    game.data.score = 0

    // add our HUD to the game world
    if (typeof this.HUD === 'undefined') {
      this.HUD = new UIContainer()
    }
    me.game.world.addChild(this.HUD)

    // display if debugPanel is enabled or on mobile
    if ((me.plugins.debugPanel && me.plugins.debugPanel.panel.visible) || me.device.touch) {
      if (typeof this.virtualJoypad === 'undefined') {
        this.virtualJoypad = new VirtualJoypad()
      }
      me.game.world.addChild(this.virtualJoypad)
    }

    // play some music
    me.audio.playTrack('dst-gameforest')
  }

  /**
   *  action to perform on state change
   */
  onDestroyEvent() {
    // remove the HUD from the game world
    me.game.world.removeChild(this.HUD)

    // remove the joypad if initially added
    if (this.virtualJoypad && me.game.world.hasChild(this.virtualJoypad)) {
      me.game.world.removeChild(this.virtualJoypad)
    }

    // stop some music
    me.audio.stopTrack('dst-gameforest')
  }
}

Let’s imagine what the PlayScreen component might look like one day:

import {Stage, Audio, Level} from 'melon-solid'
import {createSignal} from 'solid-js'
import HUD from './HUD'
function PlayScreen {
    game.data.score = 0
    // Below, our components all automatically register and unregister themselves using a combination of Solid's Lifecycle API and the Context API
    return <Stage autoPlay>
        <HUD/>
        <Level name="map1" default/>
        {(me.plugins.debugPanel && me.plugins.debugPanel.panel.visible) || me.device.touch ? <VirtualJoypad/> : null}
        <Audio track="dst-gameforest" autoPlay/>
    </Stage>
}

Then our game component would be as simple as:

<PlayScreen/>
<Entity name="FlyEntity" class={FlyEnemyEntity}/>

Future Feature Idea: On the fly level loading & Deep Linking

Another cool thing is that in the future we could use client side routing to only load the level we’re currently on. This reduces the bundle size significantly, and enables deep-linking in your game. So for example your main level’s url might look like this:
https://oursite.com/game/hub

Now once we are ready for the next level, we request it from the server and our URL changes using JS, but the page doesn’t refresh (so we don’t have to reload Melon.js). Our URL now becomes this:
https://oursite.com/game/level-one

This also has some other cool implications, like if you’re playing a multiplayer game and you want to join your friends session, they could give you a link directly into the correct map with the URL, and provide the session ID as a query string.
https://oursite.com/game/batcave?sessionID=12414

This could also enable things like bookmarking locations in the game, using the back/foward buttons in your browser to navigate between areas. This could also be a protection against data mining attacks, in a lot of games people will explore the source code to figure out hidden or unreleased features of a game. If people don’t have access to a page/area (because they haven’t unlocked it or otherwise don’t have permission), they won’t be able to see whats inside of it (allowing it to be accessed only by a select few)

Notes

I’m sorry if I got anything wrong about Melon.js, I’m a bit of a newbie in the game dev space. My hope is to combine the best of web development to game development, to make deploying games in the browser a breeze. If you like Melon-Solid, feel free to star it on Github, or even make a pull request to make it even better!

1 Like