Engine에서의 JavaScript
Engine의 스크립트 API: 생명주기, 좌표 함정, 오브젝트를 만드는 세 가지 방법, 그리고 벽을 통과하지 않고 오브젝트를 움직이는 방법.
한 줄로#
모든 엔티티는 JavaScript 스크립트를 가질 수 있습니다. 이는 브라우저 안, 사용자의 탭 안에서, 살아있는 씬을 상대로 실행되며, 표준(canonical) 스크립팅 API입니다: BASIC 방언, 그리고 에디터 안의 Python, C#, Rust 뷰는 모두 같은 표면으로 컴파일됩니다.
전체 목록은 API 레퍼런스입니다: 50개 네임스페이스에 걸친 538개의 멤버가 각각 한 페이지씩, 검색 기능과 함께 있습니다. 이 페이지는 그것을 읽기 전에 알아야 할 내용입니다.
생명주기#
function start() {} // once, when the entity starts
function update(dt) {} // every frame, dt in seconds
function fixedUpdate(dt) {} // fixed timestep, for physics
function onCollisionEnter(other) {} // first frame of contact
function onCollisionStay(other) {} // every frame while touching
function onCollisionExit(other) {} // contact just ended
function onTriggerEnter(other) {} // entered a trigger volume
function onTriggerExit(other) {}
function onMessage(msg, data, senderId) {} // entity-to-entity messaging
function onAnimationEvent(name, data) {}
function onDestroy() {}
function main() {} // console-game entry, async allowed
deltaTime, time, entityId, entity는 전역 변수이며, getComponent(type)은 스크립트를 소유한 엔티티에서 컴포넌트를 읽습니다.
충돌과 트리거는 2D와 3D 모두에서 발생하며, 각 쪽은 자신만의 접촉 노멀을 받습니다: other.contact.normal은 상대 엔티티에서 이 엔티티 쪽을 향합니다. 이것이 스크립트가 "내가 그 위에 착지했다"와 "그것이 나에게 떨어졌다"를 구분할 수 있게 해주는 것이며, 플랫포머의 밟기 판정은 other.contact.normal.y > 0.5를 읽습니다.
두 가지 좌표 함정#
둘 다 실제 디버깅 시간을 잡아먹으며, 둘 다 소리 없이 실패합니다.
마우스: 페이지 좌표 대 캔버스 좌표#
input.mousePosition은 페이지 좌표입니다. scene.pick()은 캔버스 좌표를 원합니다. 하나를 다른 것 대신 넘기면 빗나가는 게 아니라, 창 안에서 뷰포트의 위치만큼 어긋난 지점을 맞춥니다. 그래서 클릭 이동 캐릭터가 그럴듯하지만 틀린 곳으로 걸어갑니다.
인자 없이 scene.pick()을 호출하거나(커서를 겨냥함), 실제 숫자가 필요할 때는 input.mouseViewport를 사용하세요.
가상 컨트롤은 캔버스 픽셀 단위입니다#
input.addVirtualJoystick(id, x, y, size)와 input.addVirtualButton(...)은 중심을 mouseViewport와 같은 공간에 놓습니다. 조이스틱은 데드존 반경을 거친 정규화된 X/Y를 돌려주고, 버튼은 held, pressed-this-frame, released-this-frame을 각각 노출합니다. 이들의 포인터는 캡처되어 소비되므로, 컨트롤 위의 터치가 원시 캔버스 액션으로도 함께 보고되는 일은 없습니다.
기본 조이스틱은 통합되어 있습니다.
input.getJoystickX/Y()는 화면 위의 스틱, 물리 게임패드, 또는 키보드(WASD/화살표) 중 가장 강한 신호 하나를 완전한 벡터로 골라줍니다. 그래서 연결되어 있지만 유휴 상태인 패드가 키보드 이동을 막지 않습니다. Y축은 2D 화면 공간을 따릅니다: 위쪽이-1입니다.
오브젝트를 만드는 세 가지 방법, 그리고 이들은 서로 바꿔 쓸 수 없습니다#
| 무엇을 만드는가 | 얼마나 오래 사는가 | |
|---|---|---|
scene.createEntity(def) | 저작된(authored) 엔티티: Scene Graph와 Inspector에 나타남 | 프로젝트, 저장됨 |
game.spawn(type, pos) | 원시적인(bare) 프리미티브: 체력도, AI도, 행동도 없음 | 플레이 세션 |
game.spawnFrom(template, pos, opts) | 엔티티 전체의 복사본: 컴포넌트, 피직스 바디, 태그, 레이어, 스케일, 컴파일된 행동 스택 | 플레이 세션 |
game.spawn은 잔해나 프로토타입에는 적합하지만 적에게는 쓸모없습니다. 이것으로 채운 스포너는 반응 없는 공을 만들어낼 뿐입니다. spawnFrom이 복사하는 템플릿은 에디터에서 구성해 계층의 눈 아이콘으로 끄는 평범한 엔티티입니다. 별도의 포맷도, 두 번째 에디터도 없으므로, 보이는 그대로가 곧 나타나는 결과입니다.
// Once, at start: a missing template should say so, not fail every wave silently
if (!game.hasTemplate('Skeleton')) console.warn('no Skeleton template');
const mob = game.spawnFrom('Skeleton', { x, y, z }, { tag: 'Enemy' });
template은 id, 이름, 또는 태그를 받습니다. 플레이 중에 만들어진 복사본은 플레이 범위에 속하므로, stop()이 이들을 제거하고 편집 씬이 오염되는 일은 없습니다.
엔티티를 끈다는 것은 숨김과 침묵을 함께 뜻합니다: 노드 비활성화, 스크립트 정지, 더 이상 반응하지 않는 피직스 바디까지. 숨기기만 하면 보이지 않는 적이 여전히 공격해올 것입니다.
game.setEnabled(id, on)은 스크립트에서, 눈 아이콘은 계층에서 이를 제어합니다.
무언가를 움직이기: 바디를 밀어라, 순간이동시키지 마라#
매 프레임 transform.position을 쓰면 벽을, 다른 몬스터를, 바닥을 그대로 통과해 걸어갑니다. 대신 velocity를 구동하되, 구동할 대상이 있을 때만 그렇게 하세요.
if (physics.hasBody) {
const v = physics.getVelocity();
physics.setVelocity({ x: vx, y: v.y, z: vz }); // pushes a real body
} else {
transform.position.x += vx * dt; // legitimate for a flyer
}
바디 없이 physics.setVelocity를 호출하면 아무도 통합하지 않는 필드를 채울 뿐이고, 엔티티는 그 자리에서 멈춥니다. physics.hasBody가 존재하는 이유가 이것이며, 카탈로그 안의 모든 이동 행동이 답을 가정하는 대신 질문을 던지는 이유도 이것입니다. 멈춘다는 것은 velocity를 취소한다는 뜻입니다: 아무것도 하지 않으면 바디는 계속 미끄러져 갑니다.
이제는 position을 쓰면 실제로 피직스 바디가 움직입니다. Havok이 트랜스폼을 구동하지, 그 반대가 아닙니다. 그래서 다이내믹 바디 위에서
transform.position을 대입하는 것은 예전에는 다음 스텝에서 덮어써졌습니다: 물리 엔티티에서의 모든 순간이동이 소리 없이 실패했습니다. position 프록시는 이제 회전 프록시가 항상 해왔던 것처럼 바디를 다시 동기화합니다.
패스파인딩은 스냅샷입니다#
pathfinding.createGrid()는 호출된 곳을 중심으로 그리드를 한 번 레이캐스트합니다. 그 박스를 벗어나면 null을 돌려주고, 대부분의 호출자는 그러면 아무 말 없이 벽을 뚫고 지나가는 직선 경로로 대체합니다.
그리드는 기본적으로 요청받은 이동마다 다시 중심을 잡습니다. setAutoRecenter(false)는 고정된 월드에서 이를 사용하지 않도록 하는데, 그런 곳에서는 다시 만드는 것이 아무 의미 없이 레이캐스트 한 번의 비용을 치르기 때문입니다.
console.log는 기록합니다. Print는 그립니다.#
콘솔은 다른 어디에서나처럼 console.log / warn / error입니다. BASIC의 Print는 화면에 그리며 매 프레임 호출됩니다. 둘은 의도적으로 분리된 명령이며, 이 둘을 헷갈리는 것이 콘솔을 가득 채우는 원인입니다.
스크립트가 실행되는 곳#
에디터와 웹 빌드에서 스크립트는 브라우저에서 실행됩니다. 네이티브 데스크톱 또는 모바일 빌드에서는 내장된 QuickJS 런타임 위에서 변경 없이 실행되며, 이는 같은 소스이지 포팅된 것이 아닙니다.
스크립트는 편집하는 동안 브라우저 자체 데이터베이스에 저장되므로, 저장되지 않은 버퍼도 새로고침에서 살아남습니다. 저장하면 나머지 모든 것과 함께 프로젝트 안으로 들어갑니다.
한계와 흔한 함정#
- 인자가 있는
scene.pick()은 캔버스 좌표를 원합니다. 위에서 다뤘듯, 가장 흔한 위치 오류입니다. game.spawn은 캐릭터가 아니라 프리미티브를 만듭니다.spawnFrom을 쓰세요.- 게임 도중에 태어난 엔티티에도 바디가 생성됩니다. 예전에는 피직스가 Play 시점에만 구축되어서, 그 이후에 스폰된 것은 손으로 배치된 이웃들이 잘 작동하는 동안에도 소리 없이 월드를 뚫고 떨어졌습니다.
- 여덟 단계는 AI 팀원의 한계이지, 사용자의 한계가 아닙니다. 스크립트 루프에는 그런 제한이 없습니다. 폭주하는
while은 다른 곳에서와 마찬가지로 프레임을 멈춰버립니다. - 나머지 네 언어 뷰는 이것으로부터 트랜스파일됩니다. Python이나 Rust 뷰를 편집하고 다시 돌아오면 저장된 JavaScript를 거칩니다. 그 이유는 BASIC 페이지를 참고하세요.