Pyxen Script
Pyxenのゲームは、ゲーム向けに作られたPythonの方言であるPyxen Scriptで書きます。インデント、def、リスト、辞書、f文字列、内包表記と、Pythonの文法と書き心地をそのまま備えています。違うのはその下の部分です。コードはプロジェクト全体としてチェックされ、ゲームが始まる前にコンパイルされます。そして、ゲームをどのデバイスでも同じように動かすという、ひとつの仕事のために作られたランタイムで実行されます。
Pythonをすでに知っているなら、書いたコードのほとんどは期待どおりに動きます。このページでは、Pyxen Scriptがなぜあるのか、Pythonと何が共通なのか、そして意図的に何を省いているのかを説明します。
なぜ方言なのか
ゲームが言語に求めるものは、スクリプトが求めるものとは違います。
- どこでも同じように動く。 同じコンパイル済みプログラムが、iPadとMacのアプリ、Windows向けに書き出したゲーム、そしてWebエクスポートを通じてブラウザで動きます。Pyxen Scriptは最初からWebAssemblyを前提に設計されているので、ブラウザは二番手の環境ではありません。
- ガベージコレクタによる停止がない。 Pyxen Scriptにはガベージコレクタがありません。フレーム中に作られた一時的な値は、そのフレームの終わりに一度で空になるメモリ領域に置かれます。グローバル変数に入れたリストのように保持し続ける値は、どこからも参照されなくなった時点で解放されます。後片付けのためにゲームがフレームの途中で止まることはありません。
- エラーはゲームが動く前に。 最初のフレームの前に、プロジェクト全体がチェックされます。名前のつづり間違い、存在しないモジュール関数の呼び出し、Pyxen Scriptがサポートしていない構文は、ゲームが始まる前にその行で報告されます。
- エラーは正しい行に。 ゲームの実行中に問題が起きると、エラーは発生したその場所で、そこに至る呼び出しの連鎖がそのままの状態で記録されます。だからPyxenは正確な行を示せます。
以前のPyxenはMicroPythonインタプリタを使っていました。Pyxen Scriptはそれに代わるものです。
Pythonと同じところ
Pyxen Scriptは、何百もの1行の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文字列(
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に飲み込まれて、3つ先のレベルで黙っておかしな動きをするゲームになることはありません。
ゲームのコードにある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ビットです。 - インデントのタブ。インデントにはスペースを使います。タブの幅はエディタによって変わるので、それに頼るブロックは人によって違う意味になりかねません。
これらはすべて、ゲームが始まる前に、名前とその行で報告されます。
モジュールとimport
プロジェクトはひとつのプログラムとしてコンパイルされます。その中のスクリプトは、ファイル名ですでにお互いから見えています。monsters.pyにある関数spawn()は、ほかのどのスクリプトからもimportなしでmonsters.spawn()として呼べます。エンジンのAPI(world、input、Spriteなど)も常に使えます。
from pyxen import *やimport monstersのような、すでに書いてあるimportはそのまま動きます。Pyxen Scriptはそれらを不要なものとして扱うので、削除してもかまいません。