🎮 Walimaker - 专为教学设计的Python游戏引擎
Walimaker 是一个专为教学场景设计的Python游戏引擎模块。它采用简洁直观的API设计,让学生能够快速上手游戏开发,同时掌握编程核心概念。模块基于Pygame和Pymunk构建,提供完整的2D游戏开发功能。
📖 目录
- ✨ 特性亮点|🚀 快速开始|🧭 中文报错提示
- 📁 项目结构|🧰 本地开发与测试
- 📖 API 参考:窗口与主循环、Character/对象模型、物理引擎、约束、相机、TileMap、Sprite、TextBox、输入、音频、资源、报错助手、字体、世界与全局状态、场景级工具
- 🚦 高级用法:多世界、相机手感、防隧穿、约束、TileMap、动画状态机、资源管理、中文字体
- 🍳 常见任务速查|📊 性能优化建议|❓ 常见问题
- 🧪 示例代码|🚀 下一步计划|🧭 坐标系说明
- 🤝 贡献指南|📄 许可证|📞 支持与反馈
✨ 特性亮点
🎯 教学友好
- 简洁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 |
质量 / 弹性(0 |
读写 |
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) # 被打中时抖一下
📊 性能优化建议
最佳实践
- 资源复用:尽量复用Character对象,避免频繁创建销毁
- 动画优化:使用精灵表和状态机,减少Draw Call
- 物理优化:将静态物体设为STATIC类型,减少物理计算
- 内存管理:使用resource_manager的预加载功能
- 批处理:相似的对象尽量使用相同的材质和大小
常见性能问题
-
问题: 游戏卡顿,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 交互。
🤝 贡献指南
欢迎贡献代码!请遵循以下步骤:
- Fork项目
- 创建功能分支 (
git checkout -b feature/AmazingFeature) - 提交修改 (
git commit -m 'Add some AmazingFeature') - 推送分支 (
git push origin feature/AmazingFeature) - 开启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)
| File | Size | Uploaded | |
|---|---|---|---|
| walimaker-3.1.2.tar.gz | 273.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|