kitao / pyxel

A retro game engine for Python
MIT License
15.35k stars 847 forks source link

[Feature request] Implement an equivalent method to Pico 8 Load() #503

Closed salazarbarrera closed 8 months ago

salazarbarrera commented 1 year ago

Note: At least I think it's a feature not implemented yet, correct me if I'm wrong.


Would it be possible to implement a method similar to Load() that allows to play or run a pyxel program from within another pyxel program? This would allow to make custom launchers and other kind of experiments. I know it would be trivial to create a standalone launcher that uses the sys module to run pyxel under the hood, but that wouldn't be an option for browser-based releases (which in turn allow to create mobile-compatible releases), nor an efficient solution (an instance of the platform would be loaded every time a script in run).

I tried to directly use the run_python_script() method from cli.py, to directly use runpy and to directly import other pyxel programs, but in the best case scenario, I only could get the following runtime error:

"an `EventPump` instance is already alive - there can only be one `EventPump` in use at a time."

Because of this, I guess an alternative to having a Load() interface could be a way to destroy the EventPump (I'm not sure if that's possible) or allow the creation of independent processes.

Right now the alternative I see is to create third party launchers (like using a GUI to execute commands on desktop or making a website that loads the games inside iframes), but nothing native.


Edit: I just realized that the problem originates in the Rust core. I guess it could be possible to tell the core to use the same Platform singleton, or at least use the same instance of sdl_event_pump, but I know almost nothing about Rust, so what I just wrote could be just nonsense as far as I know. I guess the third party launcher is the way to go for now. Here's the error I get when I tried to call pyxel.cli.run_python_script() from a Pyxel program running in the Web Launcher:

thread '<unnamed>' panicked at 'called `Result::unwrap()` on an `Err` value: "an `EventPump` instance
 is already alive - there can only be one `EventPump` in use at a time."',
 /Users/takashi/Library/CloudStorage/OneDrive-個人用/projects/pyxel/crates/pyxel-core/src/platform.rs:88:55
pyodide.asm.js:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
pyodide.asm.js:9 --- PyO3 is resuming a panic after fetching a PanicException from Python. ---
pyodide.asm.js:9 Python stack trace below:
pyodide.asm.js:9 Traceback (most recent call last):
pyodide.asm.js:9   File "01_hello_pyxel12.py", line 28, in update
pyodide.asm.js:9     pyxel.cli.run_python_script("02_jump_game2.py")
pyodide.asm.js:9   File "/lib/python3.11/site-packages/pyxel/cli.py", line 167, in run_python_script
pyodide.asm.js:9     runpy.run_path(python_script_file, run_name="__main__")
pyodide.asm.js:9   File "<frozen runpy>", line 291, in run_path
pyodide.asm.js:9   File "<frozen runpy>", line 98, in _run_module_code
pyodide.asm.js:9   File "<frozen runpy>", line 88, in _run_code
pyodide.asm.js:9   File "02_jump_game2.py", line 138, in <module>
pyodide.asm.js:9     App()
pyodide.asm.js:9   File "02_jump_game2.py", line 6, in __init__
pyodide.asm.js:9     pyxel.init(160, 120, title="Pyxel Jump")
pyodide.asm.js:9 pyo3_runtime.PanicException: called `Result::unwrap()` on an `Err` value: "an `EventPump`
 instance is already alive - there can only be one `EventPump` in use at a time."
module.ts:76 Uncaught 20465456
pyodide.asm.js:9 [Violation] 'requestAnimationFrame' handler took 68ms
salazarbarrera commented 12 months ago

I just realized something that may help others though. If one clears the screen and properly nulls all the objects, one can call a game from another one as long as:

  1. Both share the same dimensions
  2. One comments the pyxel.run() from the second game (to avoid calling second instances of EventPump)

It's not as straightforward as the Load() command, but it's a good workaround

salazarbarrera commented 11 months ago

Here's a proof of concept of how a launcher could be made with the current codebase. It basically takes the scripts, comments out all calls to pyxel.init() and pyxel.run(), injects code to the update method of the scripts and finally runs it. It can't adapt to different width and heights, it always runs with the default frame rate, and the size of the window and pixels is the default one too.

Everything is very hacky, and probably a re-implementation of pyxel.show() and pyxel.init() could make the task way easier, but at least a solution exist.

kitao commented 11 months ago

Thank you for the prototype. I've also experimented with restarting code in the past when I wanted to do so.

If I were to add an app load feature to Pyxel, I think I would need a method that meets the following conditions:

Works the same in both web and native environments Supports resolution for each application Can restart not only the Pyxel code but also other Python modules being used I haven't found such a method yet, but if I do, I'd like to implement this feature.

kitao commented 8 months ago

Since there is no change in the situation, I will close this issue.

merwok commented 8 months ago

The only way to do this would not be at the Python level, because of all the interpreter state (imported modules, globals…) as well as pyxel state (resolution, palette…), so I can only see using os.execl to execute a clean new process, replacing the current one.

The only thing missing for a pico8-style function would be a way to pass info about the caller, so that the loaded game can provide a way to go back to the original. That could be done with an environment variable for example.