원문 캡처 · bs.ell.dev
원문 캡처 · bs.ell.dev

Bullet Script(bs)는 shmumps에서 탄막 패턴을 Lisp와 비슷한 문법으로 작성하고 실행하는 가상 머신(VM)이다. 설정 메뉴에서 난이도와 박자 시계를 조정하고, 코드 편집기에서 패턴을 실행하거나 파일로 저장할 수 있다.

스크립트는 (input 'var1 default)로 설정값을 읽는다. 설정값이 지정되지 않았으면 기본값을 사용한다.

  • 난이도는 easy, normal, hard, lunatic 중에서 고른다.
  • 박자 시계를 켜고 BPM과 오프셋 틱을 설정할 수 있다. 메트로놈도 제공한다.
  • 고급 설정에서는 회피 봇이 매 틱마다 유지할 경로 수와 탄환을 얼마나 앞서 탐색할지 정한다. 값을 높이면 더 많은 경로를 찾지만 속도는 느려진다.
  • 실행할 틱 수를 설정할 수 있다.

편집기에서 Ctrl+Enter를 누르면 파일을 불러와 실행한다. main은 다시 시작하지만 이미 발사된 탄환은 계속 움직인다. Alt+Enter는 커서 아래의 폼이나 선택한 코드를 평가하고, Ctrl+Shift+Enter는 새 가상 머신으로 처음부터 시작한다. Ctrl+Shift+H는 코드를 숨기거나 표시하며, Alt+P는 일시 정지와 재생을 전환한다. Alt+.과 Alt+,는 각각 한 틱 앞으로, 뒤로 이동한다.

F는 입력 중이 아닐 때 전체 화면을 전환한다. B 또는 cancel은 취소 가능한 탄환을 아이템으로 바꿔 없앤다. H 또는 hitbox는 탄환의 충돌 원, 레이저가 활성화된 동안의 폭, 플레이어 기체의 충돌 판정을 표시한다. D 또는 heat는 지난 1초 동안 맞을 수 있는 탄환이 있었던 위치를 열 지도로 보여준다. 최대값은 한 변이 8단위인 칸 하나에 있던 탄환 수다.

dodge를 실행하면 봇이 패턴을 새로 시작해 통과 경로를 찾은 뒤, 흔적을 남기는 유령 기체로 그 경로를 재생한다. 안전하게 이동할 수 없는 틱에는 빨간 고리가 표시된다. Escape를 누르면 중단한다. Ctrl+S는 현재 스크립트를 현재 장면에서 여는 링크를 복사하고, Ctrl+Shift+S는 유니코드 문자가 깨지는 곳에 쓸 수 있도록 같은 링크를 일반 ASCII로 복사한다. download는 코드를 .bs 파일로 저장한다. .bs 파일을 편집기에 놓으면 파일을 연다.

Shift+Alt+F는 예제 형식에 맞춰 코드를 정리한다. 본문은 두 칸 들여쓰고 인수를 정렬하며, 줄 길이는 60열 이내로 맞춘다. 기존 줄바꿈은 유지한다. bs serve를 실행한 뒤 ?serve 또는 ?serve=host:port로 열면 지정한 폴더의 파일이 Local files에 표시된다. 외부 편집기에서 파일을 저장하면 이 화면이 다시 불러오며, 화면에서 수정한 내용은 덮어쓴다. 동작은 Ctrl+Enter로 다시 불러오는 것과 같다.

언어 구성 요소

  • 함수와 기본 인수: defn, def, fn을 사용한다. (defn ring [n :speed 2 :kind 'bullet] …)처럼 정의하고 (ring 12 :speed 3)처럼 호출한다. 키워드 인수는 매개변수 목록 끝에 두며 각각 기본값을 지정한다.
  • & opts는 매개변수 목록의 끝에 두어 나머지 이름 있는 인수를 받는다. 호출의 마지막에 두면 인수를 전달한다. 예: (fire :dir d & opts).
  • [a b] 형태의 패턴으로 let, loop, doseq와 매개변수에서 목록을 분해할 수 있다. (doseq [[n speed] rows] …)처럼 쓴다. _는 값을 하나 건너뛴다.
  • let, if, when, cond, do, and, or를 지원한다. 값은 바뀌지 않는다.
  • dotimes, forever, loop, recur를 지원한다. 예: (dotimes [i n] …), (loop [a 0] (recur …)).
  • (doseq [x xs] …)는 각 항목마다 본문을 실행하며, 기다릴 수도 있다.
  • wait와 wait-until은 스크립트를 멈춘다. (wait 0.5)는 반 틱 동안 기다린다.
  • par, race, until은 폼을 함께 실행한다. race와 until은 나머지 실행을 취소한다.
  • fire는 탄환을 발사한다. (fire :dir d :speed s :kind 'k :run f)의 인수는 모두 선택 사항이다. :at은 생성 위치 오프셋, :tags는 취소용 태그, :attach는 부모에 붙이는 설정이다.
  • :motion, :delay, :cancellable을 사용할 수 있다. :motion은 'polar 또는 'wave를 받으며, 파동 움직임에는 :spin, :amp, :period를 쓸 수 있다. :delay는 탄환이 플레이어에게 피해를 줄 수 있을 때까지의 틱 수다.
  • mirror, rotate, offset, speed-scale은 본문에서 발사하는 대상을 변환한다. 예: (mirror (fan …)).
  • cancel은 탄환을 아이템으로 바꾼다. (cancel :tags [...])처럼 쓴다. move-to는 (move-to x y :over n)으로 사용한다.
  • target-x, target-y, target-count는 대상 위치를 지정한다. (aim :target i)와 (aim :lead s)도 제공한다.
  • by-diff는 설정 메뉴의 난이도를 읽는다. 예: (by-diff easy normal hard lunatic).
  • wait-beats는 다음 박자를 기다린다. (wait-beats 1)처럼 쓰며, 박자 시계는 설정에서 지정한다. 박자 시계를 설정하지 않은 경우 (wait-beats 1 30)은 30틱을 기다리고 (beat 0)은 0을 반환한다.
  • drift-x, drift-y는 accel에서 추가되는 속도다.
  • vanish는 현재 액터를 제거한다. (emit 'name)은 호스트에 이름을 전달한다.
  • change-speed, change-dir은 백그라운드에서 값을 점진적으로 바꾼다. 예: (change-speed 0 :over 40).
  • turn, accel은 각각 (turn rate :for n), (accel ax ay :over n)으로 사용한다.
  • aim, heading, speed, x, y, age로 현재 액터와 대상을 조회한다.
  • last-dir, last-speed, rand, rank를 제공한다. 마지막 발사 방향을 가져오며, 발사 전에는 (last-dir d)로 설정할 수 있다. (rand a b)는 난수를, rank는 난이도 등급을 다룬다.
  • (tick)은 코드가 실행되는 틱을 반환한다.
  • laser는 :dir, :length, :width, :warn, :active, :fade, :turn, :kind, :attach 옵션을 받는다.
  • curvy-laser는 :curve 'head 'wave 'polar, :speed, :turn, :amplitude, :period, :spin, :segments, :window를 지원한다.
  • list, nth, count, first, rest, last로 목록을 다룬다. [1 2 3]은 (list 1 2 3)과 같다. 목록은 바뀌지 않는다.
  • conj, concat, reverse, empty?, range는 기존 목록에서 새 목록을 만든다. 예: (range 0 360 30).
  • map, filter, reduce, rand-nth를 제공한다. (map (fn [x] …) xs)처럼 쓰며, 전달하는 함수는 기다릴 수 없다.
  • 기본 발사 패턴으로 ring, fan, spread를 제공한다. 예: (ring n :dir 0), (fan n :dir (aim) :step 15), (spread n :dir (aim) :width 60).
  • polygon과 rose는 (polygon sides :per-side 5 :dir 90), (rose n :petals 4 :dir 0)처럼 사용한다.
  • stream과 burst는 (stream n :dir (aim) :gap 4), (burst n :dir (aim) :width 60 :speed [1 3])처럼 사용한다. 이 패턴들은 모두 :speed(기본값 2)와 :kind를 받으며, fire가 받는 다른 키도 모든 발사에 전달한다. 예: (ring 12 :tags 'soft).
  • 산술 연산은 +, -, *, /, mod, inc, dec, abs다. 범위와 반올림에는 min, max, floor, ceil, round, trunc를 쓴다.
  • sqrt, pow, sin, cos, tan, atan2를 제공한다. 각도는 도 단위이며 0도는 +x 방향, 반시계 방향으로 증가한다.
  • lerp, clamp, <, >, =, not=, not은 보간과 비교에 사용한다.
  • (input 'var1)부터 (input 'var8)까지 설정 메뉴에서 지정한 입력을 읽는다. (input 'field-w)와 (input 'field-h)는 필드 크기를 반환한다.

실행 환경과 동작 조건

필드의 너비는 창 크기에 따라 달라지며 높이는 448단위다. 원점은 왼쪽 아래이고 y축은 위를 향한다. 발사체는 발사체를 내보내는 주체를 기준으로 상대 좌표를 유지해야 한다. 각도는 도 단위로, 0도는 오른쪽(+x), 90도는 위쪽, 270도는 플레이어 쪽 아래 방향이다. 속도 단위는 틱당 단위이며 초당 60틱으로 실행된다.

main은 적의 위치에서 적과 같은 종류로 생성된다. Ctrl+Enter로 다시 생성해도 이미 날아가는 탄환은 계속 움직인다. (aim)은 플레이어 기체를 향한다. 플레이어 기체는 마우스를 따라간다. rank는 0부터 1까지의 등급 슬라이더 값이며, var1부터 var8까지는 설정 메뉴에서 지정한다.

설정 메뉴에서 난이도(by-diff)와 박자 시계(beat, wait-beats)를 정한다. 박자 시계는 기본적으로 꺼져 있으므로 beat와 wait-beats에는 기본값을 지정해야 한다.

플레이어 기체의 충돌 판정은 반지름 2.5인 원이다. 각 종류는 스프라이트보다 조금 작은 고유 충돌 반지름을 가지며, Kinds 탭에서 항목에 마우스를 올리면 확인할 수 있다. 충돌하면 기체가 깜박이고, 탄환 하나당 한 번씩 피격 수에 반영된다. 피격으로 실행이 끝나지는 않는다. 필드 밖으로 32단위 넘게 나간 액터는 제거된다. emit으로 내보낸 이름은 화면 아래 콘솔에 표시된다.

bs serve를 실행하고 주소에 ?serve를 넣으면 디스크의 파일이 기준 파일이 된다. 파일을 저장하면 화면에서 다시 불러오며, 화면에서 수정한 내용은 대체된다. 자세한 언어 설명은 가이드에서 볼 수 있다. Kinds 탭에는 모든 스프라이트가 표시되며, 예제에서는 각 폼의 사용법을 확인할 수 있다.

종류 이름에는 어떤 기호든 사용할 수 있다. 알 수 없는 이름은 'bullet로 그려지고, 'emitter는 아무것도 그리지 않는다. 클릭하면 기호를 삽입한다.