Pyxen Script
Pyxen 游戏用 Pyxen Script 编写,这是一种为游戏打造的 Python 方言。它拥有 Python 的语法和手感:缩进、def、列表、字典、f-string、推导式。变化的是底层。你的代码会作为一个整体接受检查,并在游戏开始前完成编译,然后运行在一个只为一件事打造的运行时上:让游戏在每台设备上以同样的方式运行。
如果你已经会 Python,你写的大部分代码都会按预期工作。本页说明 Pyxen Script 为什么存在、它与 Python 有哪些相同之处,以及它有意省略了什么。
为什么是一种方言
游戏对语言的要求,和脚本不一样。
- 到处运行得都一样。 同一个编译好的程序可以在 iPad 和 Mac 的 App 中运行,在导出到 Windows 的游戏中运行,也可以通过 Web 导出在浏览器中运行。Pyxen Script 从一开始就为 WebAssembly 而设计,浏览器不是次要的目标平台。
- 没有垃圾回收造成的停顿。 Pyxen Script 没有垃圾回收器。一帧中创建的临时值放在一块内存区域里,这块区域在该帧结束时一次性清空;你保留下来的值,比如存放在全局变量里的列表,会在不再被引用时立即释放。不会有任何东西为了清理内存而让游戏在一帧中途停下来。
- 游戏运行之前就发现错误。 在第一帧之前,整个项目都会被检查。拼错的名字、调用不存在的模块函数、Pyxen Script 不支持的语法,都会在游戏启动之前就在对应的行报告出来。
- 错误定位到正确的行。 游戏运行中出了问题时,错误会在发生的地方被记录下来,通向它的调用链也完整保留,因此 Pyxen 能告诉你确切的那一行。
早期版本的 Pyxen 使用 MicroPython 解释器;Pyxen Script 取代了它。
与 Python 相同的地方
Pyxen Script 用数百个单行的 Python 惯用写法进行测试,每一个都与标准 Python 的输出对比。下面这些都与 Python 的行为一致:
- 代码块与控制流: 缩进,
if/elif/else,带break、continue和else的for与while,条件表达式(a if ok else b),链式比较(0 <= x < 320),pass,assert。 - 函数: 带默认参数和关键字参数的
def,以元组返回多个值,lambda,存放在列表和字典中的函数,嵌套函数与闭包,递归,global。 - 数据: 列表、字典(保持插入顺序)、集合、元组、切片、负索引、解包(
x, y = pos、first, *rest = items),以及列表、字典和集合推导式。 - 字符串: 带格式说明的 f-string(
f"{score:05d}"、f"{t:.2f}"),str.format,%格式化,以及常用方法(split、join、strip、replace、find、upper、startswith、zfill等)。长度和索引按字符计算,所以任何语言的文本都能正常处理。 - 内置函数:
print、len、range、enumerate、zip,支持key=和reverse=的sorted与min/max,sum、any、all、map、filter、round、abs、int、float、str、isinstance等。 - 数字: 整数是 64 位,浮点数是双精度,所以
2 ** 40和0.1 + 0.2的结果与 Python 相同。//和%向负无穷方向取整,与 Python 一致。 math: 可以使用import math,其中有sqrt、floor、sin、atan2、hypot、pi等常用函数。
不同的地方
没有类
Pyxen Script 没有 class。这是有意的设计。
在 Pyxen 游戏中,状态存放在实体及其组件上。它们归引擎所有,由引擎紧凑地存储,并在帧检查器中展示给你。类会成为存放同一份状态的第二个地方,而引擎和检查器都看不到它。所以在 Python 代码会定义一个类的地方,Pyxen 代码会生成一个实体,给它附加一个组件,再编写作用于它的函数。
一个典型的 Python 类:
class Enemy:
def __init__(self, x, y):
self.x = x
self.y = y
self.hp = 3
def hit(self):
self.hp -= 1
用 Pyxen Script 实现同样的功能:
def spawn_enemy(x, y):
e = world.spawn(name="enemy", x=x, y=y, sprite=Sprite("slime"))
e.enemy = {"hp": 3, "speed": 1.5}
return e
def hit(e):
e.enemy.hp -= 1
if e.enemy.hp <= 0:
world.destroy(e)
def update():
for e in world.all("enemy"):
e.x -= e.enemy.speed
实体本身已经有位置、精灵和在场景中的位置。enemy 组件存放敌人特有的数据,world.all("enemy") 能找到所有敌人,所以不需要维护一份要保持同步的实例列表。对于不是游戏对象的简单数据,比如高分榜或关卡描述,请使用字典和列表。完整的模型请参见实体与组件。
没有异常
Pyxen Script 没有 try、except、finally 或 raise。这同样是有意的设计。
Pyxen Script 中的错误会让游戏停止,并在对应的行报告出来,同时附上通向它的调用。中途没有任何东西能捕获它,因此错误永远不会被 except: pass 吞掉,变成一个在三关之后悄悄出问题的游戏。
游戏代码里的大多数 try 块,其实是在问一个可以直接回答的问题。那就直接问这个问题:
# 这个实体有这个组件吗?
hp = e.health.value if e.has("health") else 0
# 这个键在字典里吗?
best = scores.get(name, 0)
# 这段文本是数字吗?
if text.isdigit():
level = int(text)
# 这个索引在列表范围内吗?
if i < len(items):
item = items[i]
assert 可以使用,条件为假时会在该行停止游戏。
Pyxen Script 中也没有的
with、yield(生成器函数)、nonlocal、装饰器、async和await,以及海象运算符:=。像sum(x * x for x in xs)这样的生成器表达式可以使用。- 函数参数中的
*args和**kwargs。在调用时展开列表或字典,比如print(*items),是可以的。 - Python 标准库。可以使用
math;Pyxen 提供自己的random、time、input和storage模块,详见脚本参考。像os、sys、json或re这样的模块会在游戏运行前报告出来。 - 超过 64 位的整数。放不下的结果会以
OverflowError停止游戏,绝不会悄悄回绕。存放在组件整数字段中的数字是 32 位。 - 用 Tab 缩进。请用空格缩进:Tab 的宽度取决于编辑器,依赖它的代码块对不同的人可能意味着不同的结构。
以上每一项都会在游戏开始前,按名称在对应的行报告出来。
模块与 import
一个项目会作为一个程序来编译。其中的每个脚本已经可以通过文件名被其他脚本看到:monsters.py 中的函数 spawn(),在任何其他脚本中都可以直接以 monsters.spawn() 调用,不需要 import。引擎 API(world、input、Sprite 等)也始终可用。
你已经写好的 import,比如 from pyxen import * 或 import monsters,仍然可以正常工作。Pyxen Script 会把它们视为不必要的,你可以删掉。