Site icon Efficient Coder

How to Build a Python Game Engine with SDL, PySide6 & Lua Scripting

Building a Tiny Game Engine in Python: The HappyQQ Game Maker Walk-through

Last updated: 11 August 2025


Why read this article?

If you want to …

  • type “draw a red rectangle” in plain Chinese and see it on screen
  • embed high-speed SDL graphics inside a PySide6 window
  • let Lua scripts call your own drawing routines
  • ship the whole thing as a stand-alone EXE for Windows

… then the HappyQQ Game Maker (hqqgm) skeleton is a gentle place to start.
It is not a commercial-grade engine; it is a concise, transparent example that you can extend, cut, or re-brand.


1. What exactly is hqqgm?

In one line:

hqqgm = PySide6 main window + SDL3 render area + Lua logic + Chinese-to-Python translator + PyInstaller packager.

Module Purpose File
Qt application starts the main window, menus, resize logic app.py
SDL embed turns a Qt widget into an SDL render target sdl_embed.py
Lua bridge exposes Python functions to Lua scripts lua_bridge.py
mini interpreter converts Chinese commands to Python calls mini_interpreter.py
assets folder textures, fonts, sounds assets/

2. Five-minute quick start

These steps are copied straight from the project README; they have been tested on Windows 10 and 11 with Python 3.11.

2.1 Install the toolkit

# Windows PowerShell
py -3.11 -m venv .venv
. .venv\Scripts\Activate.ps1
python -m pip install -U pip
pip install -e .

The -e . switch installs the package in editable mode, so later code edits do not need a reinstall.

2.2 Run the default demo

python -m hqqgm

A Qt window appears with an SDL surface inside.
The title bar shows the running FPS.
Press Esc to quit.

2.3 Try the Chinese-script demo

python -m hqqgm --demo-interpreter

The console prints each Chinese line and immediately executes it.
Typical output:

清屏蓝色
draw_rect 100 100 200 200 红色

2.4 Compile a Chinese script to Python

Create example.cn.txt:

清屏蓝色
画矩形 50 50 100 100 红色

Then run:

python -m hqqgm --compile .\example.cn.txt --out .\example.gen.py

example.gen.py contains a single function:

def run(api):
    api.clear(color="blue")
    api.draw_rect(50, 50, 100, 100, color="red")

You can now:

  • run python example.gen.py directly (a tiny loader is added automatically)
  • bundle example.gen.py with your final EXE so non-programmers can change only the Chinese script and re-compile.

3. Deep dive into the folder layout

src/
└── hqqgm/
    ├── __init__.py
    ├── __main__.py       # CLI entry point, handles --compile, --demo-interpreter
    ├── app.py            # Qt side: main window, menu bar, resize events
    ├── sdl_embed.py      # SDL side: window handle, renderer, frame pacing
    ├── lua_bridge.py     # Lua side: Runtime, API registration, traceback
    ├── mini_interpreter.py  # tokenizer → AST → Python code
    └── assets/           # optional; png, ttf, wav

Tip
Keep paths relative:
Path(__file__).parent / "assets" / "myfont.ttf"
This makes both local runs and PyInstaller builds find the files without extra configuration.


4. How SDL lives inside Qt

4.1 Core idea

Qt owns the window and the event loop.
SDL only renders inside a child widget.

Steps (short version):

  1. Create a QWidget subclass (SdlWidget).
  2. Ask Qt for its native handle (winId() on Windows, effectiveWinId() on X11).
  3. Feed that handle to SDL_CreateWindowFrom.
  4. Create an SDL renderer and draw whenever Qt says “repaint”.

4.2 Minimal code excerpt

# sdl_embed.py
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QWidget
import sdl3 as sdl

class SdlWidget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_OpaquePaintEvent)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.render)

    def showEvent(self, event):
        self.sdl_window = sdl.SDL_CreateWindowFrom(int(self.winId()))
        self.renderer = sdl.SDL_CreateRenderer(self.sdl_window, None, 0)
        self.timer.start(16)  # ~60 FPS

    def render(self):
        sdl.SDL_SetRenderDrawColor(self.renderer, 0, 0, 0, 255)
        sdl.SDL_RenderClear(self.renderer)
        # call lua or translator here
        sdl.SDL_RenderPresent(self.renderer)

Note
On macOS you need the NSView handle, not NSWindow.
The demo repository currently shows only the Windows path.


5. Lua ↔ Python communication

5.1 Install lupa

pip install lupa

5.2 Expose Python functions

# lua_bridge.py
from lupa import LuaRuntime

lua = LuaRuntime(unpack_returned_tuples=True)

def draw_rect(x, y, w, h, color):
    # forward to SDL
    ...

lua.globals()['draw_rect'] = draw_rect

5.3 Sample Lua script

-- game.lua
draw_rect(50, 50, 100, 100, "red")

Run it:

with open("game.lua") as f:
    lua.execute(f.read())

6. The Chinese-to-Python interpreter

6.1 Goal in plain words

Let non-coders write:

循环 100 次
    清屏黑色
    画圆 随机位置 半径=5 颜色=随机
    延迟 16
结束

6.2 Translation pipeline

  1. Tokenise by whitespace and punctuation.
  2. Recognise “verb + arguments”.
  3. Map verbs to Python method calls.

6.3 Live examples

Chinese command Generated Python
清屏蓝色 api.clear(color="blue")
画矩形 10 20 30 40 红色 api.draw_rect(10,20,30,40,color="red")

7. Creating a stand-alone EXE

7.1 PyInstaller one-liner

pip install pyinstaller
pyinstaller -F -w -n HappyQQGameMaker src/hqqgm/__main__.py

Flags explained:

Flag Meaning
-F single file
-w no console window
-n final EXE name

7.2 Common pitfalls

Symptom Fix
SDL DLL not found add --add-binary "path_to_sdl3.dll;."
Qt platform plugin missing use --onedir first, then trim unneeded files
Hard-coded paths detect sys._MEIPASS at runtime

8. Extending the skeleton

Below are realistic next steps that stay within the scope of the original code.

8.1 Richer Chinese syntax

Add variables and if-statements:

如果 分数 > 100 则
    打印 "You win!"
否则
    打印 "Keep trying!"
结束

8.2 Hot-reload

Watch the script file for changes and re-compile on the fly.

8.3 Scene tree

Wrap every drawable in a Node class, similar to Godot’s approach.

8.4 Audio support

Link SDL_mixer in the same way SDL renderer is linked.

8.5 Visual editor

Drag-and-drop nodes, then generate the Chinese script automatically.


9. FAQ: Ten questions people always ask

Question Short answer
Does it run on Linux or macOS? Yes, but the window-handle code needs small tweaks.
Can I use Python 3.9? The author tested 3.11; 3.10 works; 3.9 is untested.
Can I load PNG images? Yes—link SDL_image and add api.draw_texture(...).
Does Chinese scripting support if/else? Not yet; the parser is intentionally minimal.
Can Lua trigger Qt signals? Yes—expose a Python wrapper that emits the signal.
Can I embed a web view? Replace the SDL widget with QWebEngineView; same idea.
What frame rate can I expect? 60 FPS with thousands of rectangles on a mid-range laptop.
May I use it in a commercial product? MIT license—go ahead, just keep the copyright notice.
Is there a debugger? Use VSCode; break points in Python work, Lua tracebacks are printed.
Any plans for Android? Possible with Qt for Android and SDL’s Android port, but extra work.

10. Recap

HappyQQ Game Maker shows you four concrete things:

  1. SDL and Qt can share the same window; the handle is the bridge.
  2. Lua and Python talk through lupa in a handful of lines.
  3. A Chinese-to-Python translator is just a lexer and a dictionary.
  4. PyInstaller can wrap everything into one EXE—the real trick is managing file paths.

Clone the skeleton, delete what you do not need, and grow the rest.
Have fun building—and playing—your own tiny game engine.


End of article

Exit mobile version