Pyxen Script
Pyxen 게임은 게임을 위해 만든 Python 방언인 Pyxen Script로 작성합니다. 들여쓰기, def, 리스트, 딕셔너리, f-문자열, 컴프리헨션까지 Python의 문법과 느낌을 그대로 갖추고 있습니다. 달라지는 것은 그 아래입니다. 코드는 프로젝트 전체가 한꺼번에 검사되고 게임이 시작되기 전에 컴파일되며, 단 하나의 일을 위해 만든 런타임에서 실행됩니다. 바로 모든 기기에서 게임을 똑같이 실행하는 일입니다.
Python을 이미 알고 있다면 작성하는 코드 대부분이 예상대로 동작합니다. 이 페이지에서는 Pyxen Script가 왜 있는지, Python과 무엇을 공유하는지, 그리고 무엇을 의도적으로 뺐는지 설명합니다.
왜 방언인가
게임이 언어에 요구하는 것은 스크립트가 요구하는 것과 다릅니다.
- 어디서나 똑같이 실행됩니다. 같은 컴파일된 프로그램이 iPad와 Mac의 앱, Windows용으로 내보낸 게임, 그리고 웹 내보내기를 통한 브라우저에서 실행됩니다. 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-문자열(
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비트입니다. - 들여쓰기의 탭. 들여쓰기는 공백으로 하세요. 탭의 너비는 에디터마다 다르므로, 탭에 의존하는 블록은 사람마다 다른 의미가 될 수 있습니다.
이 항목들은 모두 게임이 시작되기 전에 이름과 해당 줄로 보고됩니다.
모듈과 import
프로젝트는 하나의 프로그램으로 컴파일됩니다. 그 안의 각 스크립트는 파일 이름으로 이미 서로에게 보입니다. monsters.py에 있는 함수 spawn()은 다른 어떤 스크립트에서든 import 없이 monsters.spawn()으로 호출합니다. 엔진 API(world, input, Sprite 등)도 항상 쓸 수 있습니다.
from pyxen import *나 import monsters처럼 이미 작성해 둔 import는 그대로 동작합니다. Pyxen Script는 이를 불필요한 것으로 취급하므로 지워도 됩니다.