Skip to main content

🎮 Walimaker - 专为教学设计的Python游戏引擎

Python Version License Code Style

Walimaker Logo

Walimaker 是一个专为教学场景设计的Python游戏引擎模块。它采用简洁直观的API设计,让学生能够快速上手游戏开发,同时掌握编程核心概念。模块基于Pygame和Pymunk构建,提供完整的2D游戏开发功能。

📖 目录

✨ 特性亮点

🎯 教学友好

  • 简洁API:基于Python语法糖,类似Processing/turtle的直观接口
  • 错误友好:详尽的错误提示和调试信息,帮助初学者快速定位问题
  • 渐进学习:从简单绘图到复杂游戏逻辑的平滑过渡

🛠 功能完备

  • 🎨 图形渲染:精灵动画、位图渲染、TileMap支持
  • ⚙️ 物理系统:刚体动力学、碰撞检测、约束(Pymunk:connect() 刚性杆、Spring() 弹簧)
  • 🎵 音频管理:背景音乐、音效播放、音量控制
  • 📝 UI组件:文本框、对话框
  • ⌨️ 输入系统:鼠标与键盘事件
  • 📊 资源管理:智能 LRU 缓存,自动内存管理

🚀 开发效率

  • 调试工具:调试绘制 debug()、自动刷新 tracer()、移动限速 speed()、性能基准脚本
  • 类型友好:随包提供 py.typed,编辑器与 mypy 能直接提示参数类型
  • 测试与门禁:8 个测试文件 + scripts/check.py(一条命令跑 black / flake8 / mypy / 全部测试, 并支持 --matrix 自动跑 pymunk 6/7 双版本)
  • 模块化设计:对象模型、精灵动画、物理、渲染、资源各司其职,便于定制

🚀 快速开始

安装

通过pip安装

pip install Walimaker

第一个游戏:移动的角色

from Walimaker import *

# 初始化游戏窗口
setup(800, 600)
title("我的第一个Walimaker游戏")

# 创建角色(size 决定刚体形状;只给图片则不会创建刚体)
character = Character(["idle_1.png", "idle_2.png", "idle_3.png"], size=(32, 32))
character.scale(2, 2)

# 想让它受重力就加一行(默认零重力,y 轴向上所以取负值)
set_gravity(0, -1200)

# 主游戏循环
while True:
    # 事件处理:key_pressed 接收 pygame 的按键常量
    # (K_UP / K_a / K_SPACE 等常量随 `from Walimaker import *` 一起提供)
    if key_pressed(K_UP):
        character.forward(5)
    if key_pressed(K_LEFT):
        character.left(5)
    if key_pressed(K_RIGHT):
        character.right(5)
    if key_pressed(K_SPACE):
        character.say("跳跃!")

    # 动画播放
    character.play_anim()
    
    # 渲染更新
    update()

🧭 中文报错提示(教学特性)

程序出错时,英文原始报错会完整保留(异常类型、消息、完整 traceback),中文解释追加在它的后面, 方便初学者对照着官方报错一起看:

Traceback (most recent call last):
  File "main.py", line 12, in <module>
    if key_pressed(K_SPACE):
NameError: name 'K_SPACE' is not defined

────────────────────────────────────────────────────────────────
【Walimaker 中文提示】按键/事件常量「K_SPACE」没有导入
  原因:pygame 的按键、鼠标、事件常量(K_SPACE、KEYDOWN、QUIT、MOUSEBUTTONDOWN 等)需要先导入才能使用。
  解决:① 确认 walimaker 版本 ≥ 2.1.0(pip install -U walimaker),这些常量会随 from Walimaker import *
        一起提供;② 或者显式写 from pygame.locals import *。
  参考:CHANGELOG 2.1.0「恢复 pygame 事件/按键常量的导出」
────────────────────────────────────────────────────────────────

覆盖的常见错误包括:缺少依赖、变量/函数未定义(按键常量有专项提示)、忘记调用 setup()、 图片/音乐路径找不到、没有声卡、pymunk 坐标类型错误、pymunk 版本兼容问题、key_pressed("w") 传了字符串、 图片类型不支持、setup() 参数不对、Tab 与空格混用、语法/缩进错误、除零、全局变量作用域、递归过深等; 如果报错发生在 Walimaker 自身代码里,会提示"可能是框架问题 + 如何反馈"。

关闭方式(默认开启):

WALIMAKER_ERROR_HELP=0 python main.py     # 环境变量
set_error_help(False)                     # 或在代码里关闭

在 try/except 里主动取中文解释:

try:
    character = Character("hero.png")
except Exception as e:
    print(explain_exception(e))           # 返回中文解释文本

📁 项目结构

Walimaker/
├── 📄 pyproject.toml        # 打包 / 依赖配置
├── 📄 README.md             # 项目说明
├── 📄 LICENSE               # MIT 许可证
├── 📄 OPTIMIZATION_PLAN.md  # 性能 / 结构分析与优化计划
├── 📁 benchmarks/           # 性能基线脚本(不参与打包)
│   └── 🐍 perf_baseline.py
├── 📁 examples/             # 示例代码:物理引擎、约束、瓦片地图(含示例 TMX 关卡)
│   ├── 🐍 physics_basics.py
│   ├── 🐍 physics_constraints.py
│   ├── 🐍 tiledmap_basics.py
│   └── 📁 assets/           # level.tmx + tiles.png
├── 📁 tests/                # 冒烟 / 回归测试(不参与打包)
│   └── 🐍 smoke_test.py
└── 📁 src/
    └── 📁 Walimaker/        # 包源码(import Walimaker)
        ├── 📄 API.py             # 主要API接口(Character、窗口管理等)
        ├── 📄 sprite.py          # 精灵和动画系统
        ├── ⚙️ physics.py         # 物理引擎封装(基于Pymunk)
        ├── 📷 camera.py          # 相机系统(缩放、跟随)
        ├── 🗺️ tiledmap.py        # TileMap地图系统
        ├── 📝 textbox.py         # UI文本组件
        ├── 🎵 music.py           # 音频管理系统
        ├── 📦 resource_manager.py # 资源管理(智能缓存)
        ├── 🔧 config.py          # 全局配置和初始化
        ├── 🎮 test.py            # 示例代码(walimaker-demo 入口)
        └── 📁 static/            # 资源文件(只有体积很小的图片)
            └── 🖼️ *.png          # 图片资源

关于字体:从 2.3.0 起包内不再携带字体文件(原先的楷体/宋体合计 28.6 MB, 使 wheel 接近 16 MB,且属于微软版权字体不允许再分发)。中文显示改为运行时自动解析, 详见下文「🅰️ 中文字体」一节。

开发时可用 pip install -e .(或把 src/ 加入 PYTHONPATH)后 import Walimaker。

🧰 本地开发与测试(uv,推荐)

项目已按 uv 配好开发环境:一条命令建好隔离环境并装齐依赖 ([dependency-groups] dev:black / flake8 / mypy / pytest / twine)。

uv sync                                  # 创建 .venv(Python 版本由 .python-version 指定)并安装依赖
uv run python scripts/check.py           # 静态检查 + 全部测试(等价于 CI)
uv run python scripts/check.py --matrix  # 额外跑 pymunk 6.11.1 / 最新版 双版本矩阵
uv run python tests/smoke_test.py        # 也可以只跑单个测试
uv run python benchmarks/perf_baseline.py
  • uv.lock 随仓库提供(133 个包,按 Python 3.8.1+ 全范围解析),uv sync 按锁文件精确安装; 升级依赖用 uv lock --upgrade。
  • --matrix 用 uv run --no-project --with pymunk==6.11.1 现开临时环境,两个版本各跑一遍全部测试, 不需要手动准备两个 venv、也不会污染当前环境(整轮约 15 秒)。
  • 不想用 uv 也可以:pip install -e ".[dev]" 后 python scripts/check.py,效果相同。

Python 支持矩阵(requires-python = ">=3.8.1"):

版本 说明
3.8.1 ~ 3.8.x 可用,但会被解析到 pymunk 6.x(pymunk 7 要求 ≥ 3.9);3.8.0 不受支持(没有任何 flake8 版本可用)
3.9 ~ 3.13 推荐区间:pymunk 7 + 官方 pygame wheel 齐备
3.14+ 官方 pygame 尚无 wheel,需改用 pygame-ce

📚 核心模块详解

模块 职责 主要入口
config.py 世界状态、初始化、坐标换算、更新容器 World / global_var / resolve_world / init / to_cp
API.py 公共 API 门面 + 场景级工具函数 setup / update / bgpic / preload / tracer …
objects.py 对象模型(组合下面三个 mixin) NewGameObject / Character / Sensor / Wall / Mouse
sprite.py 精灵与五种动画策略 Sprite / SpriteSheet / EasySpriteStrategy / ListSpriteStrategy / AnimatorStrategy / TiledMapStrategy
animation.py collision.py input.py 对象模型的动画 / 碰撞 / 鼠标交互(mixin) 由 NewGameObject 组合,一般不直接用
physics.py 刚体、刚体组、瓦片刚体、物理子步 Body / BodiesGroup / TiledMapBodies / TiledMapBodiesGroup
constraints.py 约束(关节) connect / Connect / Spring
camera.py 相机:跟随、缩放、抖动 Camera(每个世界一个:world.CAMERA)
tiledmap.py TMX 地图解析、对象层 TiledMap
screen.py 窗口、主循环、固定物理步与子步 setup / update / done / save_screen / title / Screen
textbox.py 文本框与对话框 TextBox / DialogBox
events.py 鼠标 / 键盘 / 文本输入 key_pressed / get_mouse_pos …
music.py 背景音乐与音效 bgmusic / music_* / play_snd
resource_manager.py lru_cache.py 资源缓存(图片/字体/音效 LRU;mask 弱引用) ResourceManager / LRUCache / preload
error_help.py 中文报错助手 explain_exception / set_error_help …
fonts.py 字体解析链(包内 → 系统 → pygame 默认) font_info / resolve_font
facilitate.py draw.py draw_options.py 小工具:符号函数、线段绘制、物理调试绘制 sign / draw_line / NewDrawOptions
from Walimaker import *

# —— 最小可运行骨架 ——
setup(800, 600)              # 建窗口、相机、精灵组、物理空间
title("我的游戏")

hero = Character("hero.png", size=(32, 32))   # 传 size 才有刚体
hero.goto(0, 0)
set_gravity(0, -1200)                          # 默认零重力,需要自己开

while True:
    update()                 # 推进一帧:物理 → 逻辑 → 渲染

📖 API 参考

全部 86 个框架名字 + 558 个 pygame 事件/按键常量都随 from Walimaker import * 提供。 下面按模块给出用法;标 world= 的接口都可以省略该参数(省略即默认世界 global_var)。

🧭 概述与坐标系

Walimaker 使用笛卡尔坐标系,世界原点 (0, 0) 在窗口中心,y 轴向上、x 轴向右; 角度 0° 指向右侧、逆时针为正。窗口坐标与世界坐标用下面两个函数互转:

pygame2Cartesian(x, y)     # 窗口坐标 -> 世界坐标((x-W/2, H/2-y))
Cartesian2pygame(x, y)     # 世界坐标 -> 窗口坐标
to_cp(pos)                 # 把 Vector2/序列转成 pymunk 能接受的 (x, y) 元组(框架内部用)
random_pos()               # 在世界范围内随机取一个坐标
sign(n)                    # n 的符号:1 / -1 / 0

🖼️ 窗口与主循环

函数 说明 参数 返回值
setup(width, height, world=None) 初始化一个世界:窗口、相机、精灵组、物理空间 宽、高(整数像素) Screen
update(world=None) 推进一帧(物理 → 逻辑 → 渲染) - -
done(world=None) 进入死循环,直到关窗 - -
title(text) 设置窗口标题 文本 -
save_screen(path, world=None) 把下一帧画面保存为图片 路径 -
init() 初始化 pygame / mixer / font(幂等,一般由 setup() 调用) - -
Screen(size, world=None) 屏幕对象(world.SCREEN),有 update() / draw() - -
cwd 包目录(Walimaker/static 的父目录) - str
audio_available 音频设备是否可用(无声卡时为 False,不影响图形) - bool
__version__ 版本号,例如 "3.1.0" - str

🎮 Character / 对象模型

构造(三种贴图形式,size 决定是否创建刚体):

方式 例子 说明
单图 Character("hero.png", size=(32, 32)) size=(w, h) 矩形刚体;size=16 半径 16 的圆
帧动画 Character(["a.png", "b.png"], size=(32, 32)) 列表 = 逐帧播放
状态动画 Character({"idle": [...], "walk": [...]}, size=(32, 32)) 字典 = 状态机
瓦片地图 Character(tiledmap, size=objs) 用地图对象层当碰撞体

⚠️ 要参与物理(重力 / 碰撞 / 速度),必须传 size:只给图片不会创建刚体, velocity / apply_force() / mass 等都不会生效(框架会给一次中文提示); 另外 setup() 之后默认重力是 (0, 0),记得 set_gravity(0, -1200)。

同族类:Sensor(imgs, size)(穿透型触发器)、Wall(imgs, size)(STATIC,不动)、 Mouse(imgs)(跟随鼠标)、NewGameObject(...)(自定义 body_type 的基类)。 底层载体是 GameObject(obj._gameObject):它持有 sprite、刚体、瓦片刚体与坐标, NewGameObject 只是它的易用封装;需要直接操作精灵/刚体时可以从这里进去。

属性 类型 说明 权限
x, y, pos float / vec 世界坐标 读写
dir vec 朝向向量(默认 (1, 0)) 读写
rot float 旋转角度(度) 读写
velocity vec 线速度(无刚体时无效) 读写
angular_velocity float 角速度 读写
mass / elasticity / friction float 质量 / 弹性(01) / 摩擦(01) 读写
red green blue alpha / color int / tuple 着色与透明度 读写
visible bool 是否显示与参与渲染 读写(show() / hide())
width, height int 贴图尺寸 只读
layer int 渲染层级 读写(move_to_front() / move_to_back())
frame int 当前动画帧 读写
state str 当前动画状态(下一帧生效) 读写
dt float 动画帧间隔(仅图片列表策略) 读写
name / properties / tmx_object str / dict / 对象 TileMap 对象层附加信息 读写
方法 说明 参数
forward(d) / backward(d) 沿朝向前进/后退 d 像素(受 speed() 限速) 像素
left(a) / right(a) 相对朝向左/右转 a 度 角度
goto(x, y) / goto((x, y)) 瞬移到坐标 坐标
slide_to(pos, v=1) 朝目标匀速滑动 坐标、速度
face_to(x, y) 朝向某个坐标 坐标
scale(sx, sy=None) / body_scale(sx, sy) 缩放显示 / 缩放刚体尺寸 倍数
flipx(bool) / flipy(bool) 水平/垂直翻转 布尔
apply_force(x, y) 施加冲量(框架沿用"力"的叫法) 向量
collide(target) 碰撞检测:目标可以是坐标或另一个对象 坐标/对象 → bool
separate(target) "刚刚分开"的那一帧 坐标/对象 → bool
distance(target) 到目标的距离 坐标/对象 → float
say(text, textColor=(0,0,0)) 弹出对话框(say() 无参关闭) 文本
play_snd(path, volume=None) / set_volume(v) 播放音效 / 设置音量 路径、0~1
play_anim() / stop_anim() 播放 / 暂停动画 -
set_dt(state, dt) 状态动画的帧间隔(state=None 表示全部状态) 状态名、秒
set_next_state(from, to) 播完 from 自动切到 to 状态名
set_start_func(state, fn) / set_end_func(state, fn) 进入状态 / 播完一轮的回调 状态名、函数
get_mouse_clicked(b="all") 鼠标是否按在该对象上 按键号
get_mouse_just_clicked(b="all") 刚刚按下 按键号
get_mouse_just_released(b="all") 刚刚松开 按键号
get_mouse_upon() 鼠标是否悬停在贴图上 -
kill() 销毁对象(连同刚体、对话框) -

🧱 物理引擎

set_gravity(0, -1200, world=None)     # 设置重力(默认 (0, 0))
debug(world=None)                     # 打开物理调试绘制(画出刚体形状)
名字 说明
Body(body_type, shape, size, sensor, world=None) 单个刚体。body_type:"STATIC" / "KINEMATIC" / "DYNAMIC";shape:"CIRCLE"(size=半径) / "BOX"(size=(w,h)) / "POLY"(size=顶点列表)
BodiesGroup() 刚体更新组(每帧同步显示坐标)。世界里的实例:world.BODIES
TiledMapBodies(body_type, objs, sensor, world=None) 由一组对象(各自带 .points)生成的瓦片刚体集合
TiledMapBodiesGroup() 瓦片刚体更新组:world.TMBG
world.SPACE 底层 pymunk.Space(要自己 step()/add() 时用)

防隧穿(高速物体穿墙):物理是固定步长 1/60 秒推进的,一步位移超过碰撞体厚度就会穿过去。 框架按"最快物体"自动切分物理子步:

global_var.MAX_STEP_DISTANCE = 8.0   # 每个子步允许的最大位移(像素);0 = 关闭子步
global_var.MAX_SUBSTEPS = 16         # 子步数上限

🔗 约束(关节)

名字 说明
connect(a, b, world=None) 刚性杆(pymunk.PinJoint),保持创建时的距离;返回该关节
Connect(a, b, world=None) 同上(类形式)
Spring(a, b, rest_length=None, stiffness=100, damping=10, anchor_a=(0,0), anchor_b=(0,0), world=None) 弹簧-阻尼;rest_length=None 取当前距离,之后可直接改 spring.rest_length

a / b 可以是游戏对象,也可以是物理层的 Body。

📷 Camera 相机

每个世界一个相机:global_var.CAMERA 或 World().CAMERA。

成员 说明
follow(subject) 跟随某个对象(subject 需要有 pos)
deadzone = (半宽, 半高) 跟随死区:主体在死区内移动时相机不动(默认 (0, 0) 严格居中)
scale(s) / zoom_to(s, speed=2.0) 直接设置缩放 / 平滑缩放到 s
smooth_zoom = True 缩放时用 pygame.transform.smoothscale(更平滑、稍慢)
shake(amount=8, duration=0.3) / stop_shake() 屏幕抖动 / 立即停止
x, y, pos, offset, size, scl 位置、贴到窗口的偏移、尺寸、缩放
surface 相机画面(update() 后可直接 pygame.image.save)

🗺️ TiledMap 瓦片地图

成员 说明
TiledMap("map.tmx") 解析 TMX(会读图集,需要 setup() 之后再创建)
tiled.width / tiled.height 地图像素尺寸
bgpic(tiled) 把地图铺成背景并渲染(相机尺寸自动 = 地图大小),返回背景对象
tiled.render(surface) / tiled.make_map() 把瓦片层画到指定 surface / 生成整张地图 surface
tiled.objects / tiled.get_objects(name=None) 对象层的解析结果(obj.name/type/x/y/points/properties;坐标已换算)
tiled.create_objects(name=None, body_type="STATIC", sensor=False, images=None, world=None) 把对象层实例化为游戏对象:矩形/多边形变 POLY 刚体,TMX 图块对象自动带贴图

🎨 Sprite 精灵(更底层的接口)

平时用 Character 就够了;需要自定义绘制/组合时可以直接操作精灵。

名字 说明
Sprite(imgs, world=None) 精灵:imgs 可以是 Surface / 路径 / 列表 / 状态字典 / SpriteSheet / TiledMap
Sprite.autos 类型 → 策略的映射表(可自行注册新策略)
EasySpriteStrategy(img) 单帧策略
ListSpriteStrategy(imgs) 帧动画策略(frame / set_dt / playing)
AnimatorStrategy(imgs_dict) 状态动画策略(state / set_dt(state, dt) / set_next_state / set_start_func / set_end_func)
TiledMapStrategy(tiledmap) 把整张 TMX 当作一帧
SpriteSheet(sheet, w, h) 把一张图按 w×h 网格切成多帧(sheet[i])
sprite.is_flip_h / is_flip_v / red / green / blue / alpha / visible 视觉状态
sprite.image_changed / moved 本帧是否变化(渲染优化用)

📝 TextBox / DialogBox

名字 说明
TextBox(size, font=None, bgfill=None, font_color=(0,0,0), world=None) 屏幕文字;font=None 自动选字体(见下文「🅰️ 中文字体」)
text.write(content) / text.print(content) 设置文本(print 是兼容别名)
text.pos / text.goto(x, y) / text.color 位置与颜色
DialogBox(parent, img, font, world=None) 跟随对象的对话框(一般通过 obj.say() 使用)

🖱️⌨️ 输入(events)

函数 说明 返回值
get_mouse_pos(world=None) 鼠标位置(世界坐标,同一帧内只查询一次) tuple
get_mouse_rel(world=None) 本帧鼠标移动量 vec
get_mouse_clicked(b="all", world=None) 鼠标按住 bool
get_mouse_just_clicked(b="all", world=None) 鼠标刚按下 bool
get_mouse_just_released(b="all", world=None) 鼠标刚松开 bool
set_mouse_visible(visible) 显示/隐藏光标 -
key_pressed(key=None, world=None) 按住某键;不传参 = 是否有任意键按下 bool
key_just_pressed(*keys, world=None) 某键刚按下;传多个键 = 是否都刚按下;不传参返回本帧按下的键列表 bool / list
key_just_released(*keys, world=None) 某键刚松开(同上) bool / list
key_input(world=None) 本帧输入的文本(TEXTINPUT 事件) str

按键常量(K_UP、K_a、K_SPACE、KEYDOWN、QUIT …)都随 from Walimaker import * 提供。

🎵 音频(music)

函数 说明
bgmusic(path, loops=-1, start=0.0) 播放背景音乐(默认循环)
set_volume(v) 背景音乐音量(0.0~1.0)
music_load(path) / music_queue(path) 加载 / 排队下一首
music_play(loops=-1, start=0.0) / music_stop() / music_pause() / music_unpause() / music_rewind() 播放控制
music_fadeout(ms=1000) 淡出
music_set_volume(v) / music_get_volume() / music_get_busy() 音量与状态
music_set_pos(seconds) / music_get_pos() 播放位置
music_set_endevent(event_id=None) / music_get_endevent() 播放结束时投递的事件(None = 取消)

音效:obj.play_snd(path, volume=None)(每个对象最多同时保留 20 路、 obj.set_volume(v) 设置默认音量);world 里的 audio_available 可判断有没有声卡。

📦 资源管理

名字 说明
preload(assets, world=None) 批量预加载:{"images": [...], "fonts": [(None, 24)], "sounds": [...]}
load_image(path, world=None) 加载图片(带缓存)
ResourceManager(max_cache_size=1000) 资源管理器(单例);world.RESOURCE_MANAGER
.load_image(path) / .load_font(path, size) / .load_sound(path) 单资源加载(LRU 缓存)
.get_mask(surface, threshold=0) 像素 mask(按 Surface 弱引用缓存)
.preload(assets) / .unload(path) / .clear_cache(kind=None) / .get_cache_info() 预加载 / 卸载 / 清缓存 / 看统计
LRUCache(maxsize) 通用 LRU 缓存(get / put / clear / info)

🧭 中文报错助手(error_help)

名字 说明
explain_exception(exc) 在 try/except 里主动获取中文解释(英文原文保留)
explain_exception 的开关:set_error_help(True/False) / is_error_help_enabled() 运行时开关
install_error_help() / uninstall_error_help() 手动安装 / 卸载 sys.excepthook(导入包时已自动安装)

环境变量 WALIMAKER_ERROR_HELP=0 可整体关闭。

🅰️ 字体解析(Walimaker.fonts)

名字 说明
font_info(path=None) 打印当前字体来源与 has_cjk,排查"中文显示成方框"
resolve_font(path=None) / resolve_font_path(path=None) 解析字体(显式路径 → 环境变量 → 包内 → 系统 → pygame 默认)
reset_font_cache() 清理字体解析缓存

环境变量 WALIMAKER_FONT 可指定字体文件(机房统一字体很方便)。

🌍 世界与全局状态(config)

名字 说明
global_var 默认世界(World 实例),所有 world= 参数的缺省值
World() 新建一个独立世界(自己的窗口状态、相机、精灵组、物理空间)
resolve_world(world=None) 把 None 解析成默认世界(框架内部统一用它)
world.WIDTH / HEIGHT 窗口尺寸
world.ALL_SPRITES / GROUP / BODIES / TMBG 精灵组 / 对象更新组 / 刚体组 / 瓦片刚体组
world.SPACE / CAMERA / SCREEN / RESOURCE_MANAGER 物理空间 / 相机 / 屏幕 / 资源管理器
world.EVENTS / FRAME / LINES / FORCE_REDRAW / DEBUG_DRAW 本帧事件、帧号、待绘线段、重绘标记、调试绘制
world.SPEED / IS_UPDATED speed() 限速、tracer() 自动刷新
world.MAX_STEP_DISTANCE / MAX_SUBSTEPS 防隧穿子步参数
world.set_value(name, value) / get_value(name) 按名字读写世界属性
Group(update_hook=None) 按帧更新的容器(world.GROUP 用对象协议,刚体组用钩子)

🛠 场景级工具

名字 说明
tracer(enable, world=None) 打开后每次 update() 结束再自动刷新一次(适合演示/白板)
speed(s, world=None) 设置 forward() / left() 等阻塞式移动的每步位移上限
debug(world=None) 打开物理调试绘制
draw_line(p1, p2, color, width, world=None) 在下一帧的叠加层画一条线
set_depth(i=1) 返回 "../" * i(不再改变工作目录,需要时自己 os.chdir)
Color / Rect pygame 的类型(随之导出,方便直接用)

🚦 高级用法

🌍 多世界(互不干扰的两套场景)

每个 World 拥有自己的窗口状态、相机、精灵组、物理空间与更新组。 所有接口的 world 参数都可以省略,省略即默认世界(global_var),所以旧代码完全不用改。

from Walimaker import *
from Walimaker.config import World

# 默认世界(老写法,等价于 world=None)
setup(800, 600)
hero = Character("hero.png", size=(32, 32))

# 第二个世界:自己的相机 / 空间 / 精灵组
w2 = World()
setup(800, 600, world=w2)
ghost = Character("ghost.png", size=(32, 32), world=w2)
text = TextBox(24, world=w2)

update()          # 只推进默认世界
update(w2)        # 只推进 w2;两个世界的物理与画面互不影响

ghost.velocity = vec(50, 0)   # 只影响 w2 里的刚体
hero.kill()                   # 只移除默认世界里的对象
  • 可用 world 的接口:setup / update / done / save_screen、Screen、Camera、 Character / Sensor / Wall / Mouse / NewGameObject、TextBox / DialogBox、Sprite、 Body / TiledMapBodies、draw_line / set_gravity / debug / tracer / speed / random_pos / bgpic、 鼠标与键盘查询函数。
  • 资源缓存(ResourceManager)是进程内共享的:图片/字体只解码一次,两个世界共用。
  • 相机不再全局唯一:World().CAMERA 属于各自的世界,Camera((宽, 高)) 会新建实例。

📷 相机手感(抖动 / 跟随死区 / 平滑缩放)

from Walimaker import *

hero = Character("hero.png", size=(32, 32))
camera = global_var.CAMERA

camera.follow(hero)                     # 跟随角色
camera.deadzone = (40, 30)              # 在 80x60 的死区内移动时相机不动(默认 0 = 严格居中)
camera.zoom_to(2.0, speed=1.5)          # 平滑缩放到 2 倍(每秒变化 1.5)
camera.smooth_zoom = True               # 缩放时用 smoothscale(更平滑,但比 scale 慢)
camera.shake(amount=8, duration=0.3)    # 被打中 / 爆炸时抖一下

这些都默认关闭(deadzone=(0,0)、smooth_zoom=False、不抖动),因此不影响既有项目。

⚡ 高速物体穿墙怎么办(防隧穿)

物理是固定步长(1/60 秒)推进的,而 pymunk/Chipmunk 没有连续碰撞检测(CCD): 一步的位移超过碰撞体厚度时,物体会直接穿过去(实测 10px 厚的墙在 1200 px/s 时就会穿模)。

框架默认按“最快的物体”自动切分物理子步,保证每个子步的位移不超过 8 像素:

global_var.MAX_STEP_DISTANCE = 8.0   # 每个子步允许的最大位移(像素);0 = 关闭子步
global_var.MAX_SUBSTEPS = 16         # 子步数上限,防止极端速度把一帧切成几百步
# 自定义世界:w = World(); w.MAX_STEP_DISTANCE = 4.0
  • 慢速场景子步数恒为 1,行为与性能都不变(默认 8px 对应约 480 px/s 以下);
  • 超过 MAX_SUBSTEPS × MAX_STEP_DISTANCE × 60(默认约 7680 px/s)仍可能穿模—— 这类极端速度请调大上限,或把碰撞体做厚一些(薄墙 + 极高速是最容易穿的组合)。

🔗 把两个物体连起来(约束)

from Walimaker import *

a = Character("a.png", size=(32, 32))
b = Character("b.png", size=(32, 32))
a.goto(-50, 0)
b.goto(50, 0)

connect(a, b)                                  # 刚性杆:保持当前距离(pymunk.PinJoint)
Spring(a, b, rest_length=100, stiffness=200)   # 弹簧:把距离拉回 rest_length(DampedSpring)

两者都接受游戏对象或物理层的 Body,并自动把关节加到对象所属世界的空间; 距离/劲度/阻尼可以直接读改(spring.rest_length = 80)。

🗺️ TileMap:铺成背景 / 取出对象层

from Walimaker import *

tiled = TiledMap("map.tmx")

# 1) 把整张地图当背景铺上(相机尺寸自动设成地图大小),返回背景对象
bg = bgpic(tiled)

# 2) 把对象层里的对象变成可碰撞的游戏对象(每个对象一个 POLY 刚体)
#    TMX 图块对象会自动带上贴图;也可以用 images={"对象名": 图片} 指定
objects = tiled.create_objects()                    # 全部
spawns = tiled.create_objects(name="spawn")         # 按名字过滤
for obj in objects:
    print(obj.name, obj.properties, tuple(obj.pos))

# 想自己建对象也行:直接看解析结果(几何 + 属性)
for obj in tiled.get_objects():
    print(obj.name, obj.x, obj.y, obj.points)

状态机与动画控制

from Walimaker import *

character = Character({
    "idle": ["idle_1.png", "idle_2.png"],
    "walk": ["walk_1.png", "walk_2.png", "walk_3.png"],
    "jump": ["jump_1.png", "jump_2.png"]
})

# 设置动画参数
character.set_dt("walk", 0.1)  # 走路动画每帧0.1秒
character.set_next_state("walk", "idle")  # 走路结束后回到空闲状态

# 状态切换回调
def on_jump_start():
    print("开始跳跃!")
    
def on_jump_end():
    print("跳跃结束!")
    character.state = "idle"

character.set_start_func("jump", on_jump_start)
character.set_end_func("jump", on_jump_end)

# 切换状态
character.state = "walk"  # 开始走路
character.state = "jump"  # 开始跳跃(触发回调)

资源管理

from Walimaker import *
from Walimaker.config import global_var

# 预加载资源(顶层 API,避免第一次使用时卡顿)
preload({
    "images": ["player.png", "enemy.png", "background.png"],
    "fonts": [(None, 24), (None, 36)],          # None = 自动选择中文字体
    "sounds": ["jump.wav", "collect.wav"]
})

# 查看缓存状态
cache_info = global_var.RESOURCE_MANAGER.get_cache_info()
print(f"已缓存图片: {cache_info['images']['current_size']}张")
print(f"已缓存字体: {cache_info['fonts']['current_size']}种")

# 清理缓存
global_var.RESOURCE_MANAGER.clear_cache("image")  # 仅清理图片缓存

图片/字体/音效用 LRU 缓存;碰撞 mask 用弱引用缓存——同一个 Surface 只构建一次, Surface 被回收后缓存条目自动消失,不会把临时 Surface 一直留在内存里。

🅰️ 中文字体

从 2.3.0 起包内不再携带字体文件(原先的 simkai.ttf + simsun.ttc 合计 28.6 MB, 使 wheel 接近 16 MB,且属于微软授权字体不允许再分发)。TextBox / DialogBox 的字体改为运行时按顺序解析:

顺序 来源 说明
1 显式传入路径 TextBox(25, font=r"C:\Windows\Fonts\simhei.ttf")
2 环境变量 WALIMAKER_FONT 机房统一字体:set WALIMAKER_FONT=D:\fonts\SourceHanSansSC.otf
3 包内 Walimaker/static/ 把任意 ttf/ttc/otf 丢进去即可,无需改代码
4 操作系统中文字体 Windows 微软雅黑/黑体/宋体;macOS 苹方/冬青黑体;Linux Noto CJK/文泉驿
5 pygame 默认字体 任何平台都存在,但不含中文字形(中文显示成方框)
  • 找不到字体不会报错:只发出一次中文 + 英文的 RuntimeWarning,程序继续运行; 仅当“显式指定的字体文件确实存在但已损坏”时才抛出错误。

  • 排查中文显示成方框:

    from Walimaker.fonts import font_info
    print(font_info())
    # {'path': '/System/Library/Fonts/Hiragino Sans GB.ttc', 'source': 'system',
    #  'source_label': '操作系统中文字体', 'has_cjk': True}
    

    若 has_cjk 为 False,说明系统里没有中文字体(常见于精简 Linux / Docker 镜像), 安装 fonts-noto-cjk(Debian/Ubuntu:apt install fonts-noto-cjk)或设置 WALIMAKER_FONT 指向一个中文字体文件即可。只跑逻辑、不显示画面 (SDL_VIDEODRIVER=dummy)的场景不受影响。

🍳 常见任务速查(Cookbook)

from Walimaker import *

setup(800, 600)

① 显示一张图片 / 精灵

bgpic("bg.png")                              # 背景(也可以是 Surface 或 TiledMap)
hero = Character("hero.png", size=(32, 32))  # size 决定刚体形状
hero.goto(0, -100)

② 按键控制移动(带限速)

speed(6)                       # forward/left/right 的每步位移上限
while True:
    if key_pressed(K_LEFT):  hero.left(4)
    if key_pressed(K_RIGHT): hero.right(4)
    if key_pressed(K_UP):    hero.forward(6)
    update()

③ 让角色受重力并站到地面上

set_gravity(0, -1200)
ground = Wall(size=(700, 40)); ground.goto(0, -250)
hero = Character("hero.png", size=(32, 32)); hero.goto(0, 0)
while True:
    if hero.collide(ground):                  # 是否接触(也可传坐标)
        if key_pressed(K_SPACE):
            hero.velocity = (0, 620)          # 向上跳(y 轴向上)
    update()

④ 碰撞时做事("刚刚撞上" / "刚刚分开")

if hero.collide(coin):          # 一直重叠时会持续为 True
    coin.kill(); score += 1
if hero.separate(ground):       # 只在离开地面那一帧为 True
    print("起跳!")

⑤ 显示与更新文字(HUD)

hud = TextBox(24)
hud.write(f"分数 {score}")
hud.goto(-350, 250)             # 世界坐标

⑥ 播放音乐与音效

bgmusic("bgm.ogg")              # 循环背景音乐
set_volume(0.6)
hero.play_snd("jump.wav")       # 每个对象最多同时 20 路

⑦ 截图 / 保存某一帧

save_screen("shot.png")         # 下一帧 update() 时保存
update()

⑧ 计时与节奏(不依赖真实时间)

if global_var.FRAME % 60 == 0:  # 约每秒一次(帧率上限 60)
    spawn_enemy()

⑨ 多世界(互不干扰的两套场景)

w2 = World()
setup(800, 600, world=w2)
ghost = Character("ghost.png", size=(32, 32), world=w2)
update()          # 默认世界
update(w2)        # 第二个世界

⑩ 瓦片地图三件事

tiled = TiledMap("level.tmx")
bgpic(tiled)                                     # 铺成背景
solids = tiled.create_objects(name="ground_body")  # 对象层 -> 碰撞体
coins  = tiled.create_objects(name="coin_a")       # 图块对象自动带贴图
spawn  = tiled.get_objects("spawn")[0]             # 取出生点坐标

⑪ 绳索 / 弹簧

connect(anchor, box)                                  # 刚性杆
spring = Spring(anchor, ball, rest_length=100)        # 弹簧
spring.rest_length = 140                              # 随时改自然长度

⑫ 相机跟随 + 受击抖动

cam = global_var.CAMERA
cam.follow(hero)
cam.deadzone = (40, 30)
cam.shake(amount=8, duration=0.3)     # 被打中时抖一下

📊 性能优化建议

最佳实践

  1. 资源复用:尽量复用Character对象,避免频繁创建销毁
  2. 动画优化:使用精灵表和状态机,减少Draw Call
  3. 物理优化:将静态物体设为STATIC类型,减少物理计算
  4. 内存管理:使用resource_manager的预加载功能
  5. 批处理:相似的对象尽量使用相同的材质和大小

常见性能问题

  • 问题: 游戏卡顿,FPS下降

  • 检查: 使用debug()模式查看物理调试绘制

  • 解决: 减少同时活动的物理对象数量

  • 问题: 内存占用过高

  • 检查: 查看resource_manager缓存状态

  • 解决: 调整缓存大小或手动清理缓存

❓ 常见问题(FAQ)

Q:我用 Character("hero.png") 创建角色,为什么它不掉下来、velocity 也没反应? A:两件事要同时满足:① 创建时传 size(Character("hero.png", size=(32, 32))), 只给图片不会创建刚体;② 调 set_gravity(0, -1200)——默认是零重力。 漏掉时框架会给一次中文提示。

Q:AttributeError: 'NoneType' object has no attribute ...? A:多半是还没 setup(宽, 高)(窗口、相机、精灵组、物理空间都由它创建); 中文报错助手会直接提示。

Q:中文显示成方框? A:说明当前字体没有中文字形。先 from Walimaker.fonts import font_info; print(font_info()): has_cjk=False 时请安装系统 CJK 字体(Linux:apt install fonts-noto-cjk)、 设置环境变量 WALIMAKER_FONT,或把字体文件放进 Walimaker/static/。

Q:子弹/被击飞的箱子会穿过墙? A:物理每步位移超过碰撞体厚度就会"隧穿"。框架已按最快物体自动切分物理子步 (默认每个子步 ≤8 像素);极高速时调小 global_var.MAX_STEP_DISTANCE 或调大 global_var.MAX_SUBSTEPS,也可以把墙做厚一点。

Q:报错说找不到图片/音乐? A:相对路径是相对于运行脚本时所在的目录(不是代码文件所在目录),且区分大小写; 建议用 os.path.join(os.path.dirname(__file__), ...) 拼绝对路径。

Q:按键判断失效 / key_pressed("w") 报错? A:key_pressed 要传 pygame 的按键常量:key_pressed(K_w)(常量随 from Walimaker import * 提供), 传字符串会抛 TypeError。不传参数表示"是否有任意键按下"。

Q:怎么在服务器/CI 上跑(没有显示器)? A:需要一次 setup()(会创建窗口);无显示器环境用 SDL 的 dummy 驱动即可: SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy python your_game.py (本仓库的 tests/ 与 examples/ 都是这样跑的)。

Q:报错能不能给中文解释? A:默认已开启:英文原始 traceback 完整保留,其后追加中文的「原因 / 解决 / 参考」。 也可在 try/except 里用 explain_exception(e) 主动取解释;WALIMAKER_ERROR_HELP=0 关闭。

Q:为什么安装包很小、里面没有字体? A:2.3.0 起不再打包字体(原先的楷体/宋体合计 28.6 MB 且属微软版权字体,不能随包分发), 中文改为运行时解析系统字体,详见「🅰️ 中文字体」。


🧪 示例代码

examples/ 目录里是可直接运行的完整示例(重点演示物理引擎与瓦片地图):

uv run python examples/physics_basics.py        # 重力/三种刚体/碰撞/冲量/材质
uv run python examples/physics_constraints.py   # 刚性链 connect() + 弹簧 Spring()
uv run python examples/tiledmap_basics.py       # TMX 地图当背景 + 对象层变碰撞体/金币

它们都支持 WALIMAKER_EXAMPLE_FRAMES=N(跑 N 帧后退出),方便无显示器自测; 详细说明见 examples/README.md。

另外 Walimaker/test.py(walimaker-demo 入口)里还有一个完整的内置 demo: 平台跳跃、弹球物理、TileMap、UI 交互。

🤝 贡献指南

欢迎贡献代码!请遵循以下步骤:

  1. Fork项目
  2. 创建功能分支 (git checkout -b feature/AmazingFeature)
  3. 提交修改 (git commit -m 'Add some AmazingFeature')
  4. 推送分支 (git push origin feature/AmazingFeature)
  5. 开启Pull Request

代码规范

  • 遵循 PEP 8 编码规范
  • 使用 类型注解 (Python 3.7+)
  • 注释:重要功能和复杂逻辑需要注释
  • 测试:新增功能需包含测试用例

📄 许可证

本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情。

📞 支持与反馈

  • 文档问题 / Bug 报告:附上重现步骤、错误日志与运行环境(Python、pygame、pymunk 版本)
  • 功能建议:欢迎直接反馈
  • 版本与更新日志:见 PyPI 项目页 与仓库根目录的 CHANGELOG.md

🙏 致谢

感谢以下开源项目:

🚀 下一步计划(Roadmap)

诚信说明:早期版本的介绍里曾把下面几项列为「已实现特性」,实际尚未落地。 现在统一移到 Roadmap,未做就是未做;欢迎反馈你最想要哪一个。

  • 代码热重载(改代码即时生效,无需重启)
  • 可视化编辑器
  • 插件 / 组件系统
  • 常用 UI 控件(按钮、输入框、列表…)
  • 触控 / 移动端手势
  • WebAssembly 支持(让 Walimaker 在浏览器中运行)
  • 3D 渲染扩展(基于 OpenGL)
  • 网络多人游戏支持

🧭 坐标系说明

  • 坐标系统:笛卡尔坐标系,世界原点 (0, 0) 在窗口中心,x 向右、y 向上 (所以 goto(0, 200) 在窗口上半部分;向上跳要给正的 y 速度)
  • 窗口坐标换算:pygame2Cartesian(x, y) / Cartesian2pygame(x, y);鼠标与键盘事件给的是窗口坐标
  • 角度系统:0°指向右侧,逆时针方向角度增加
  • 颜色值范围:RGB 通道 0-255,透明度 0-255
  • 音量控制:0.0(静音)到 1.0(最大音量)
  • 物理单位:像素;默认重力是 (0, 0)(零重力),需要自己 set_gravity(0, -1200) (负值才向下,因为 y 轴向上;教学示例常用 900~1400)

教学提示:Walimaker的设计目标是让编程初学者能够在30分钟内制作出第一个可玩的游戏。从简单到复杂,循序渐进地学习游戏开发的核心概念。

Metadata

Release files for walimaker 3.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for walimaker 3.1.2
File Size Uploaded
walimaker-3.1.2.tar.gz 273.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for walimaker 3.1.2
File Interpreter ABI Platform
walimaker-3.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 498.8 kB

Release files / walimaker-3.1.2.tar.gz

Download URL walimaker-3.1.2.tar.gz
Size 273.0 kB
Tags Source
SHA-256 checksum
How to use checksums
bdf2a5753c807665bee7cc8dc22f15c59b91d8d44ceb1890fcc79073af366e69
BLAKE2b-256 checksum
How to use checksums
2eef9f39def06ac5245dea4669bee7891bec93131ce37d3acbec032c9fc043ef
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / walimaker-3.1.2-py3-none-any.whl

Download URL walimaker-3.1.2-py3-none-any.whl
Size 225.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
59923bc275db342a10bcf17677b5d8e7309397171edebf07a561f88ba98fcd02
BLAKE2b-256 checksum
How to use checksums
c2eb4e4064287d41e454b8cb93e6efc432e0d5463ccb77262687a85ccc3a3a18
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.23 {"installer":{"name":"uv","version":"0.11.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

4.0.2

2 release files

4.0.1

2 release files

4.0.0

2 release files

This release

3.1.2 This release

2 release files

3.1.1

2 release files

3.1.0

2 release files

3.0.0

2 release files

2.3.1

2 release files

2.3.0

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.0

2 release files

1.1.0

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page