maxence-charriere / go-app

A package to build progressive web apps with Go programming language and WebAssembly.
https://go-app.dev
MIT License
7.98k stars 369 forks source link

embedding and communicating with another wasm executable #783

Closed amlwwalker closed 6 months ago

amlwwalker commented 2 years ago

I am using the Golang library Ebiten to make a 2D game. I want to 'embed' this into a webpage and then build the webpage around the game using Go-App.

  1. What is the best way to do this? Is there a convention or just an iframe with the embedded game? Does using Go-App offer any benefits to do it a certain way?
  2. Can I communicate between the two wasm's somehow? Can I send data back and forth between the two in anyway, by using properties of the browser or something?
amlwwalker commented 2 years ago

ping?

oderwat commented 2 years ago

I guess nobody answered because that is not very Go-App related and quite a special use-case too.

My Ebiten game does just use an HTML page for mobile. But I can't really deploy it that way because it still uses too many resources. I wrote to and back with the author of Ebiten some months ago, but we could not identify the problem (only on mobile, not on desktop browsers). So, currently I am the only player and use the Desktop version. I was thinking about rewriting it for Go-App using Canvas. Which is very possible for that game as I render everything and implemented a GUI myself. We wrote some canvas based "pseudo" games while we were evaluation Go-App, and it works good. You simply need to set up a canvas and use the display update callback for rendering updates.

I am sure you can communicate between WASM modules the same way as you communicate with java-script objects (aka through window.data.whatever="test" / https://stackoverflow.com/questions/1063813/listener-for-property-value-changes-in-a-javascript-object).

WASM (as compiled by the Go compiler) has no idea about other modules AFAIK.

Setting it up to run multiple different WASM modules can be a bit tricky. At least, you need to know what you do. I did it with Go App as WASM and some extra modules that got compiled through TinyGo as proof of concept. That was using a game logic compiled by TinyGo and the display and key input handled by Go-App

But as you probably already need a backend for the user data, why don't just use that from both parts and embed the game as an iframe. Direct communication with the iframe should work too if you are serving is under the same (sub-)domain.

For my game, I just put everything inside Ebiten, because of the complexity.

oderwat commented 2 years ago

BTW. Here a "pong" fake rendered using canvas as Go-App component:

package pages

import (
    "math"
    "math/rand"

    "github.com/google/uuid"

    "git.jetbrains.space/metatexx/skelly/skelly/pkg/x/dbg"

    "github.com/maxence-charriere/go-app/v9/pkg/app"
)

type renderChan chan struct{}

type Canvas struct {
    app.Compo
    Width   float64
    Height  float64
    Id      string
    done    renderChan
    running bool
}

var _ app.Composer = (*Canvas)(nil)
var _ app.Mounter = (*Canvas)(nil)
var _ app.Dismounter = (*Canvas)(nil)

func (c *Canvas) OnInit() {
    dbg.Log()
    c.Id = "cvs:" + uuid.NewString()
}

func (c *Canvas) OnDismount() {
    dbg.Log()
    // tell our go routine that we don't need it anymore
    c.running = false // comment this out to see how it gets ugly :)
}

func (c *Canvas) OnMount(ctx app.Context) {
    dbg.Log()
    cvEl := app.Window().GetElementByID(c.Id)
    cvCtx := cvEl.Call("getContext", "2d")

    // run "endless"
    // Notice: I actually think the whole channel thing here is not correct, but it works(tm)
    c.done = make(chan struct{})
    ctx.Async(func() {
        scale := c.Width / 640.0
        s := 10.0 * scale
        x := float64(rand.Intn(int(640-50))+25) * scale
        y := float64(rand.Intn(int(480-10))+5) * scale
        ph := 80.0 * c.Width / 640
        speed := 200.0 * scale
        dx, dy := 0.21, 0.42 // speed
        dx += float64(rand.Intn(100)/100) - 0.5
        w := cvEl.Get("width").Float()
        h := cvEl.Get("height").Float()
        var renderFrame app.Func
        var tlast float64
        c.running = true
        var tcount = 0
        renderFrame = app.FuncOf(func(this app.Value, args []app.Value) interface{} {
            now := args[0].Float()
            tdiff := now - tlast
            tlast = now
            tcount++
            if tcount%100 == 0 {
                dbg.Logf("count %d", tcount)
            }

            cvCtx.Call("clearRect", 0, 0, w, h)
            cvCtx.Call("beginPath")
            cvCtx.Set("globalAlpha", 1)
            cvCtx.Set("lineWidth", 5*scale)
            cvCtx.Set("strokeStyle", "#202020")
            cvCtx.Set("fillStyle", "#e0e0e0")
            cvCtx.Call("rect", 0, 0, w, h)
            cvCtx.Call("fill")
            cvCtx.Call("stroke")

            cvCtx.Call("beginPath")
            cvCtx.Call("setLineDash", []interface{}{5})
            cvCtx.Set("globalAlpha", 0.5)
            cvCtx.Set("lineWidth", 3*scale)
            cvCtx.Call("moveTo", w/2+2.5, 0)
            cvCtx.Call("lineTo", w/2+2.5, h)
            cvCtx.Call("stroke")

            ppos := y - ph/2
            if ppos < 5 {
                ppos = 5
            } else if (ppos + ph) > h-5 {
                ppos = h - ph - 5
            }

            cvCtx.Call("beginPath")
            cvCtx.Call("setLineDash", []interface{}{})
            cvCtx.Set("fillStyle", "#30A030")
            //cvCtx.Set("strokeStyle", "#000000")
            cvCtx.Set("lineWidth", 2)
            cvCtx.Set("globalAlpha", 1.0)
            cvCtx.Call("rect", 15*scale, ppos, 10*scale, ph)
            cvCtx.Call("fill")
            //cvCtx.Call("stroke")

            cvCtx.Call("beginPath")
            cvCtx.Set("lineWidth", 2)
            cvCtx.Set("fillStyle", "#3030A0")
            //cvCtx.Set("strokeStyle", "#000000")
            cvCtx.Call("rect", w-25*scale, ppos, 10*scale, ph)
            cvCtx.Call("fill")
            //cvCtx.Call("stroke")

            cvCtx.Set("globalAlpha", 1.0)
            cvCtx.Call("beginPath")
            //          cvCtx.Set("lineWidth", 0)
            cvCtx.Set("fillStyle", "#A00000")
            cvCtx.Set("strokeStyle", "#000000")
            //cvCtx.Call("setLineDash", []interface{}{1})
            cvCtx.Call("setLineDash", []interface{}{})
            cvCtx.Call("arc", x, y, s, 0, 2*math.Pi)
            cvCtx.Call("fill")
            //          cvCtx.Call("stroke")
            sdx := dx * speed / tdiff
            sdy := dy * speed / tdiff

            if (x+sdx) > w-25*scale-s || (x+sdx) < s+25*scale {
                dx *= -1
                sdx *= -1
            }
            if (y+sdy) > h-s || (y+sdy) < s {
                dy *= -1
                sdy *= -1
            }
            x += sdx
            y += sdy
            if c.running {
                app.Window().Call("requestAnimationFrame", renderFrame)
            } else {
                close(c.done)
            }
            return nil
        })
        defer renderFrame.Release()
        // start the rendering
        app.Window().Call("requestAnimationFrame", renderFrame)
        // wait till the channel gets closed
        <-c.done
        dbg.Log("closed")
    })
}

func (c *Canvas) Render() app.UI {
    dbg.Log()
    if c.Width == 0 {
        if c.Height == 0 {
            c.Width = 640
        } else {
            c.Width = math.Floor(c.Height / 3 * 4)
        }
    }
    if c.Height == 0 {
        if c.Width == 0 {
            c.Height = 480
        } else {
            c.Height = math.Floor(c.Width / 4 * 3)
        }
    }
    return app.Canvas().ID(c.Id).Width(int(c.Width)).Height(int(c.Height))
}
gedw99 commented 7 months ago

you can do multi wasm using tractor. Its pretty clever

https://github.com/tractordev/wanix