I needed a way to embed Godot in .NET applications, and ended up facilitating HTML5 export for C# games, too, which really reinvigorated my passion for Godot in game jams. And for boring work, I needed parallel unit testing (across multiple ALCs).

But what is it…? I call it a C# front-end for the Godot engine, but really…

2dog is still Godot

All the Godot goodness still works, including programming with GodotSharp (their official C# SDK) and the 4.7 official editor, which was important to me for stability and trust (also, easier to swap out or extend).

The nuget packages are free and libre open source under the MIT license. 2dog uses libgodot and creates a choice of small .NET “host” applications that you can tailor to your needs - HTML5 browser export, embedding in WinForms, xUnit testing, etc.

Still hot off the press, always happy about visitors to the ⤜outfox⤏ discord or issues on our GitHub.

  • Domi@lemmy.secnd.me
    link
    fedilink
    arrow-up
    4
    ·
    11 days ago

    I tried exporting our GMTK game jam project but there’s an error when running it in browser:

    Want me to create an issue for it or is this a known issue?

    Full error
    [MONO] /__w/1/s/src/runtime/src/mono/mono/mini/aot-runtime-wasm.c:188 <disabled>
    Mc<@mono_wasm_invoke_jsexport (http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:172284
    @:wasm-function[161353]:0x27db323
    @:wasm-function[148375]:0x25e923b
    @:wasm-function[148280]:0x25e743d
    @:wasm-function[148282]:0x25e750b
    @:wasm-function[148283]:0x25e7540
    @:wasm-function[151868]:0x26965df
    @:wasm-function[147551]:0x25be71a
    @:wasm-function[147539]:0x25b1b0d
    @:wasm-function[148504]:0x25ed1d9
    @:wasm-function[147541]:0x25bdb91
    @:wasm-function[147539]:0x25b1a39
    @:wasm-function[147589]:0x25bf98f
    @:wasm-function[151658]:0x26875df
    @:wasm-function[150592]:0x2656cb5
    @:wasm-function[150591]:0x2656c71
    @:wasm-function[150687]:0x265bea9
    @:wasm-function[149718]:0x262d509
    @:wasm-function[149814]:0x263183c
    @:wasm-function[147637]:0x25c1ae8
    @:wasm-function[147549]:0x25be542
    @:wasm-function[147539]:0x25b1a07
    @:wasm-function[148285]:0x25e7584
    @:wasm-function[147548]:0x25be424
    @:wasm-function[147539]:0x25b1936
    @:wasm-function[147626]:0x25c127f
    @:wasm-function[147665]:0x25c29d2
    @:wasm-function[161284]:0x27d98bf
    @:wasm-function[105210]:0x1bc1a8b
    @:wasm-function[105221]:0x1bc46ba
    @:wasm-function[137662]:0x23c7ae1
    @:wasm-function[42805]:0xa2ed8c
    @:wasm-function[42808]:0xa30f39
    @:wasm-function[133508]:0x22bbe6f
    @:wasm-function[134260]:0x22daeb9
    @:wasm-function[137157]:0x23adb33
    @:wasm-function[87320]:0x14c7cdb
    @:wasm-function[147637]:0x25c1aca
    @:wasm-function[147549]:0x25be542
    @:wasm-function[147539]:0x25b1a07
    @:wasm-function[147589]:0x25bf98f
    @:wasm-function[151658]:0x26875df
    @:wasm-function[150592]:0x2656cb5
    @:wasm-function[150591]:0x2656c71
    @:wasm-function[150687]:0x265bea9
    @:wasm-function[149718]:0x262d509
    @:wasm-function[149814]:0x263183c
    @:wasm-function[147637]:0x25c1ae8
    @:wasm-function[147549]:0x25be542
    @:wasm-function[147539]:0x25b1a07
    @:wasm-function[147589]:0x25bf98f
    @:wasm-function[151658]:0x26875df
    @:wasm-function[150592]:0x2656cb5
    @:wasm-function[150601]:0x26574fc
    @:wasm-function[150637]:0x265991d
    @:wasm-function[161345]:0x27dae8c)
    createDotnetRuntime/Module._mono_wasm_invoke_jsexport@http://127.0.0.1:8000/_framework/dotnet.native.js:14520:195
    wn@http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:29440
    Vc/<@http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:178845
    Vc@http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:178996
    async*Godot/Module.callMain@http://127.0.0.1:8000/godot.js:70:37
    SafeEngine/start/Engine</</<@http://127.0.0.1:8000/godot.js:894:24
    promise callback*SafeEngine/start/Engine</<@http://127.0.0.1:8000/godot.js:893:25
    SafeEngine/start/Engine<@http://127.0.0.1:8000/godot.js:886:13
    promise callback*start@http://127.0.0.1:8000/godot.js:865:22
    SafeEngine/startGame/Engine<@http://127.0.0.1:8000/godot.js:932:22
    promise callback*startGame@http://127.0.0.1:8000/godot.js:931:8
    @http://127.0.0.1:8000/:146:24
    @http://127.0.0.1:8000/:161:10
    logging.ts:129:25
        Mc logging.ts:129
        <anonymous> source47:41792291
        <anonymous> source47:39752251
        <anonymous> source47:39744573
        <anonymous> source47:39744779
        <anonymous> source47:39744832
        <anonymous> source47:40461791
        <anonymous> source47:39577370
        <anonymous> source47:39525133
        <anonymous> source47:39768537
        <anonymous> source47:39574417
        <anonymous> source47:39524921
        <anonymous> source47:39582095
        <anonymous> source47:40400351
        <anonymous> source47:40201397
        <anonymous> source47:40201329
        <anonymous> source47:40222377
        <anonymous> source47:40031497
        <anonymous> source47:40048700
        <anonymous> source47:39590632
        <anonymous> source47:39576898
        <anonymous> source47:39524871
        <anonymous> source47:39744900
        <anonymous> source47:39576612
        <anonymous> source47:39524662
        <anonymous> source47:39588479
        <anonymous> source47:39594450
        <anonymous> source47:41785535
        <anonymous> source47:29104779
        <anonymous> source47:29116090
        <anonymous> source47:37518049
        <anonymous> source47:10677644
        <anonymous> source47:10686265
        <anonymous> source47:36421231
        <anonymous> source48:36548281
        <anonymous> source48:37411635
        <anonymous> source48:21789915
        <anonymous> source48:39590602
        <anonymous> source48:39576898
        <anonymous> source48:39524871
        <anonymous> source48:39582095
        <anonymous> source48:40400351
        <anonymous> source48:40201397
        <anonymous> source48:40201329
        <anonymous> source48:40222377
        <anonymous> source48:40031497
        <anonymous> source48:40048700
        <anonymous> source48:39590632
        <anonymous> source48:39576898
        <anonymous> source48:39524871
        <anonymous> source48:39582095
        <anonymous> source48:40400351
        <anonymous> source48:40201397
        <anonymous> source48:40203516
        <anonymous> source48:40212765
        <anonymous> source48:41791116
        _mono_wasm_invoke_jsexport dotnet.native.js:14520
        wn managed-exports.ts:287
        Vc managed-exports.ts:65
        Vc run.ts:71
        callMain godot.js:70
        Engine godot.js:894
        (Async: promise callback)
        Engine godot.js:893
        Engine godot.js:886
        (Async: promise callback)
        start godot.js:865
        Engine godot.js:932
        (Async: promise callback)
        startGame godot.js:931
        <anonymous> (index):146
        <anonymous> (index):161
    
    • tiger tiger tiger@programming.devOP
      link
      fedilink
      arrow-up
      4
      ·
      edit-2
      11 days ago

      Oof, looks like a Godot crash, do you have a public repo with the game jam entry somewhere? I probably have to dig a little deeper, my suspicion is something related to some kind of native function/trampoline being completely missing. (weird)

      You could also try to create a source map: dotnet publish -c Release -p:WasmEmitSymbolMap=true -p:WasmNativeStrip=false

      • Domi@lemmy.secnd.me
        link
        fedilink
        arrow-up
        3
        ·
        11 days ago

        We started in Forward+ rendering so it is definitely possible that something went wrong when switching to Compatibility renderer. I did a blank test project and everything went fine so it must be something related to the project.

        do you have a public repo with the game jam entry somewhere?

        Yes! It’s open source and I made a branch with the renderer switched to Compatibility and our water shader removed (incompatible with Compatibility renderer). Should be able to just add 2dog right away and run it. (The regular desktop build runs fine)

        You can find it here: https://gitlab.com/gmtk26zbandco/gmtk26-godot-game/-/tree/web-build

        You could also try to create a source map: dotnet publish -c Release -p:WasmEmitSymbolMap=true -p:WasmNativeStrip=false

        I added the source map and the stack trace looks more usable now:

        Full error
        [MONO] /__w/1/s/src/runtime/src/mono/mono/mini/aot-runtime-wasm.c:188 <disabled>
        Mc<@wasm_native_to_interp_GodotSharp_Godot_Bridge_ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance (http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:172284
        dotnet.native.wasm.wasm_trace_logger@:wasm-function[161353]:0x283a3f9
        dotnet.native.wasm.eglib_log_adapter@:wasm-function[148375]:0x2640662
        dotnet.native.wasm.monoeg_g_logv_nofree@:wasm-function[148280]:0x263e7bf
        dotnet.native.wasm.monoeg_g_log@:wasm-function[148282]:0x263e88f
        dotnet.native.wasm.monoeg_g_log_disabled@:wasm-function[148283]:0x263e8c4
        dotnet.native.wasm.mono_wasm_get_interp_to_native_trampoline@:wasm-function[151868]:0x26f0907
        dotnet.native.wasm.ves_pinvoke_method@:wasm-function[147551]:0x261556b
        dotnet.native.wasm.mono_interp_exec_method@:wasm-function[147539]:0x260893b
        dotnet.native.wasm.mono_thread_info_current@:wasm-function[148504]:0x26446fb
        dotnet.native.wasm.interp_throw_ex_general@:wasm-function[147541]:0x26149c6
        dotnet.native.wasm.mono_interp_exec_method@:wasm-function[147539]:0x2608867
        dotnet.native.wasm.interp_runtime_invoke@:wasm-function[147589]:0x261680e
        dotnet.native.wasm.mono_jit_runtime_invoke@:wasm-function[151658]:0x26e159e
        dotnet.native.wasm.do_runtime_invoke@:wasm-function[150592]:0x26aff97
        dotnet.native.wasm.mono_runtime_invoke_checked@:wasm-function[150591]:0x26aff53
        dotnet.native.wasm.mono_runtime_try_invoke_byrefs@:wasm-function[150687]:0x26b5306
        dotnet.native.wasm.ves_icall_InternalInvoke@:wasm-function[149718]:0x268592e
        dotnet.native.wasm.ves_icall_InternalInvoke_raw@:wasm-function[149814]:0x2689e7b
        dotnet.native.wasm.do_icall@:wasm-function[147637]:0x26189f1
        dotnet.native.wasm.do_icall_wrapper@:wasm-function[147549]:0x261538f
        dotnet.native.wasm.mono_interp_exec_method@:wasm-function[147539]:0x2608835
        dotnet.native.wasm.mono_assertion_message_disabled@:wasm-function[148285]:0x263e908
        dotnet.native.wasm.ftnptr_to_imethod@:wasm-function[147548]:0x2615271
        dotnet.native.wasm.mono_interp_exec_method@:wasm-function[147539]:0x2608764
        dotnet.native.wasm.interp_entry@:wasm-function[147626]:0x261817a
        dotnet.native.wasm.interp_entry_static_ret_4@:wasm-function[147665]:0x26198f5
        dotnet.native.wasm.wasm_native_to_interp_GodotSharp_Godot_Bridge_ScriptManagerBridge_CreateManagedForGodotObjectScriptInstance@:wasm-function[161284]:0x2838975)
        dotnet.native.wasm.CSharpScript::_create_instance(Variant const**, int, Object*, bool, Callable::CallError&)@:wasm-function[105210]:0x1bfe612
        dotnet.native.wasm.CSharpScript::instance_create(Object*)@:wasm-function[105221]:0x1c012a3
        dotnet.native.wasm.Object::set_script(Variant const&)@:wasm-function[137662]:0x2419e21
        dotnet.native.wasm.SceneState::instantiate(SceneState::GenEditState) const@:wasm-function[42805]:0xa44c80
        dotnet.native.wasm.PackedScene::instantiate(PackedScene::GenEditState) const@:wasm-function[42808]:0xa46e43
        dotnet.native.wasm.Main::start()@:wasm-function[133508]:0x230c7e8
        dotnet.native.wasm.GodotInstance::start()@:wasm-function[134260]:0x232b9bc
        dotnet.native.wasm.MethodBindTR<bool>::ptrcall(Object*, void const**, void*) const@:wasm-function[137157]:0x23ff93a
        dotnet.native.wasm.gdextension_object_method_bind_ptrcall(void const*, void*, void const* const*, void*)@:wasm-function[87320]:0x14f1f59
        dotnet.native.wasm.do_icall@:wasm-function[147637]:0x26189d3
        dotnet.native.wasm.do_icall_wrapper@:wasm-function[147549]:0x261538f
        dotnet.native.wasm.mono_interp_exec_method@:wasm-function[147539]:0x2608835
        dotnet.native.wasm.interp_runtime_invoke@:wasm-function[147589]:0x261680e
        dotnet.native.wasm.mono_jit_runtime_invoke@:wasm-function[151658]:0x26e159e
        dotnet.native.wasm.do_runtime_invoke@:wasm-function[150592]:0x26aff97
        dotnet.native.wasm.mono_runtime_invoke_checked@:wasm-function[150591]:0x26aff53
        dotnet.native.wasm.mono_runtime_try_invoke_byrefs@:wasm-function[150687]:0x26b5306
        dotnet.native.wasm.ves_icall_InternalInvoke@:wasm-function[149718]:0x268592e
        dotnet.native.wasm.ves_icall_InternalInvoke_raw@:wasm-function[149814]:0x2689e7b
        dotnet.native.wasm.do_icall@:wasm-function[147637]:0x26189f1
        dotnet.native.wasm.do_icall_wrapper@:wasm-function[147549]:0x261538f
        dotnet.native.wasm.mono_interp_exec_method@:wasm-function[147539]:0x2608835
        dotnet.native.wasm.interp_runtime_invoke@:wasm-function[147589]:0x261680e
        dotnet.native.wasm.mono_jit_runtime_invoke@:wasm-function[151658]:0x26e159e
        dotnet.native.wasm.do_runtime_invoke@:wasm-function[150592]:0x26aff97
        dotnet.native.wasm.mono_runtime_try_invoke@:wasm-function[150601]:0x26b0803
        dotnet.native.wasm.mono_runtime_invoke@:wasm-function[150637]:0x26b2ce0
        dotnet.native.wasm.mono_wasm_invoke_jsexport@:wasm-function[161345]:0x2839f50
        createDotnetRuntime/Module._mono_wasm_invoke_jsexport@http://127.0.0.1:8000/_framework/dotnet.native.js:14520:195
        wn@http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:29440
        Vc/<@http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:178845
        Vc@http://127.0.0.1:8000/_framework/dotnet.runtime.js:3:178996
        async*Godot/Module.callMain@http://127.0.0.1:8000/godot.js:70:37
        SafeEngine/start/Engine</</<@http://127.0.0.1:8000/godot.js:894:24
        promise callback*SafeEngine/start/Engine</<@http://127.0.0.1:8000/godot.js:893:25
        SafeEngine/start/Engine<@http://127.0.0.1:8000/godot.js:886:13
        promise callback*start@http://127.0.0.1:8000/godot.js:865:22
        SafeEngine/startGame/Engine<@http://127.0.0.1:8000/godot.js:932:22
        promise callback*startGame@http://127.0.0.1:8000/godot.js:931:8
        @http://127.0.0.1:8000/:146:24
        @http://127.0.0.1:8000/:161:10
        logging.ts:129:25
        

        Thanks for taking the time!

        • tiger tiger tiger@programming.devOP
          link
          fedilink
          arrow-up
          5
          ·
          edit-2
          11 days ago

          Awesome, thanks for the detailed report and prepared repository to reproduce in. The failure is a bit different on my end - first I get a bunch of issues trying to serve the blender files (which it shouldn’t, on the web, very likely my bug! bad dog, bad dog… - should already be imported), and then I get your crash. Is this what you get, too?

          Game seems to run okay in the editor. I’m working on making the blender import happen correctly or raise an error during the 2dog import step, I think this is where it goes wrong. (also, fantastic test case for future tests… I built 2dog because of resource import issues, now resource import bites me in the belly yet again, ha.)

          Edit: No, the problem is not the missing blender import, after correctly setting it to my blender path I get your behaviour. Which is great, only need to find the missing native trampoline now…

          • tiger tiger tiger@programming.devOP
            link
            fedilink
            arrow-up
            3
            ·
            edit-2
            11 days ago

            Getting closer, it’s GD.PushError and some other functions with 7 parameters under the hood. (a little confused why these aren’t generated, or how the native equivalent to GD.PushError(“Blah”) has 7 params, maybe it tries to look up all candidates) … I need to do something else for the next hour or so, and then can get back to this. My automated and manual tests always fail on errors so ironically that led to GD.PushError never being tested.

            In related news, who ever in the .NET/mono team thought it was a good idea to call native signature strings “cookies” in the context of WASM specifically deserves a villain award… I navigated away from the documentation page three times thinking it was about web cookies.

          • Domi@lemmy.secnd.me
            link
            fedilink
            arrow-up
            3
            ·
            11 days ago

            Awesome! Let me know if you need anything else and feel free to keep the project around as test case.

            I think Godot converts the Blender files internally so good to hear that it works now with the path set.

    • tiger tiger tiger@programming.devOP
      link
      fedilink
      arrow-up
      3
      ·
      11 days ago

      Yay! Your game works! *asterisk…

      I released a new release (v4.7.1.59) of 2dog that hardwires all the native WASM trampoline functions. It’s one of those bugs that has me scratching my head, as in “how did this ever work?”, but it did, including for samples from the Godot foundation, three friends’ games, and two of my own. So… mega big thanks for this bug report, this was a big one!

      You may need to run dotnet nuget locals all --clear and dotnet restore --force or similar to get the fresh packages.

      Anyway. The asterisk: the loading performance of certain scenes and textures isn’t great at all 😓, and I’m working on ways to make that nicer in the web html harness; and I also need to figure out where the majority of that time is spent; sometimes it’s WASM compile time, sometimes it’s scene load.

      PS: Super adorable game idea. That angry beach ball can get bent, though… sheesh. Ey, I’m walkin’ crawlin’ here!

      • Domi@lemmy.secnd.me
        link
        fedilink
        arrow-up
        3
        ·
        11 days ago

        Yay, that’s awesome!

        I needed to do a dotnet dnx 2dog again after your commands to regenerate the build assets but after that the build worked fine, thanks!

        The asterisk: the loading performance of certain scenes and textures isn’t great at all

        To cheer you up: The performance of the regular Godot web version is not all that great either for 3D stuff. It loaded quite quickly on my PC. Throws an error on level load but still works, might just be the shady level loading we did under time pressure.

        PS: Super adorable game idea. That angry beach ball can get bent, though… sheesh. Ey, I’m walkin’ crawlin’ here!

        Glad you liked it! We put a lot of work into this one. :)

        Shame I didn’t see 2dog during the jam but for next time we are equipped to finally ship a web version. Thanks!

        • tiger tiger tiger@programming.devOP
          link
          fedilink
          arrow-up
          3
          ·
          11 days ago

          Just try it, in the worst case you have to git reset --hard and git clean -dxn / -dxf and dnx 2dog add again, but I think the dotnet restore --force should do it! There’s only changes to the native library and the glue code.

          I plan to use 2dog a lot in future jams… it wasn’t quite stable enough for GMTK jam, though, so… something to look forward to in upcoming jams.

      • tiger tiger tiger@programming.devOP
        link
        fedilink
        arrow-up
        1
        ·
        9 days ago

        v4. 7.1.64 or later should drastically improve loading performance. I find it acceptable with it, if not outstanding of course.

        Turns out among other things, I was loading the PCK and the WASM blob twice depending on web server behaviour. 🤪