12219 단어
61 분
Alice2 리버싱 노트
AI 이 게시글은 AI가 생성한 콘텐츠를 포함합니다.
2026-06-10
태그 없음

Alice 2 - Jython 상호운용성 계층#

분석 목적: Alice 2(JAVA)에 내장된 Jython 스크립팅 시스템의 전체 구조와 유저 접점. 분석 기준: TheAliceProject/alice2 소스 코드 + Alice 2.6.1 배포본 직접 분석. 마지막 업데이트: 2026-06-10


아키텍처 개요#

flowchart TD
AT["Alice 2 저작 도구<br/>(JAlice.java)"] -->|생성| W["World"]
W -->|소유| SF["ScriptingFactory<br/>Jython 초기화, alice 패키지 로드"]
W -->|소유| I["Interpreter<br/>월드별 Jython 엔진"]
I -->|소유| NS["Namespace<br/>PyStringMap 서브클래스"]
NS -->|이름 조회| WE["PyElement / PySandbox<br/>Alice 객체 프록시"]
W -->|소유| SP["ScriptProperty<br/>캐싱 + .py 저장"]
W -->|소유| SR["ScriptResponse<br/>매 프레임 EXEC_MULTIPLE"]
W -->|소유| SDR["ScriptDefinedResponse<br/>프롤로그에서 EVAL"]
subgraph UF["4가지 사용자 입력면"]
S1["1. Sandbox Script<br/>우클릭 → Edit Script"]
S2["2. ScriptResponse 블록<br/>드래그앤드롭 → 텍스트 입력"]
S3["3. ScriptDefinedResponse 블록<br/>드래그앤드롭 → Response 표현식"]
S4["4. ScriptComboWidget + ScratchPad<br/>하단 툴바 즉시 실행"]
end
S1 -->|"World.start() 시"| I
S2 -->|"매 프레임 update(t)"| I
S3 -->|"프롤로그(t) 평가"| I
S4 -->|"유저 클릭 실행"| I
I -->|"Py.exec / Py.eval"| JR["Jython 런타임"]
JR -->|"__finditem__"| NS
JR -->|"alice.* 함수"| PK["alice Python 패키지<br/>(Required/jython/Lib/alice/)"]
PK -->|Java 상호운용| SF

⚠️ 중요: Script 기능 동작 안 함 (Alice 2.6.1)#

소스 코드 분석 결과, Interpreter.javaPy.compile_flags 호출 시 마지막 인자가 null로 전달되어 Jython 2.7.3에서 NPE가 발생. 이로 인해 모든 Script 기반 기능(Sandbox Script, ScriptResponse, ScriptDefinedResponse, ComboWidget, ScratchPad)이 실행되지 않음. 블록 기반 편집만 정상 작동.


읽기 가이드#

전체 구조부터 보고 싶다?
→ 01-architecture.md (Jython 부트스트랩, CompileType, World.start())
→ 05-behind-the-scenes.md (Java 클래스 전체 인벤토리)
이름이 어떻게 Alice 오브젝트에 연결되는지 궁금하다?
→ 02-name-resolution.md (Namespace → PyElement 전 과정, PySandbox 변수 쓰기)
유저 입장에서 뭘 입력해야 하는지 알고 싶다?
→ 03-script-surfaces.md (4가지 입력면 + 동작 안 함 확인)
API가 궁금하다?
→ 04-api-reference.md (alice.* 함수 전수 + 버그 목록)
월드 파일을 뜯어보고 싶다?
→ 06-file-format-a2w.md (a2w ZIP 구조, 저장/로드)
→ 07-file-format-a2c.md (a2c 갤러리 오브젝트)
→ 08-element-data-xml.md (elementData.xml 스키마)
→ 09-storing-scripts.md (ScriptProperty, .py 파일 저장)
블록이 어떻게 생성되는지 알고 싶다?
→ 10-drag-and-drop.md (팔레트 → ResponsePrototype → Element)
실전 문제 해결이 필요하다?
→ 11-troubleshooting.md (로딩 실패, 폰트, 변수, a2w 직접 수정)

문서 목록#

#파일내용분량
0101-architecture.mdJython 부트스트랩, World.start(), ScriptResponse/ScriptDefinedResponse
0202-name-resolution.mdNamespace → PyElement 전체 체인, PySandbox 변수 할당
0303-script-surfaces.md유저 중심: 4가지 입력면 UI + 실행 시점★ 많음
0404-api-reference.mdalice.* API 전수, 발견된 버그 3건
0505-behind-the-scenes.mdJava 클래스 계층, s_classnameMap, 호출 체인
0606-file-format-a2w.mda2w ZIP 내부, 저장/로드 플로우
0707-file-format-a2c.mda2c 갤러리 오브젝트 포맷
0808-element-data-xml.mdelementData.xml 스키마 전수
0909-storing-scripts.mdScriptProperty, .py 저장, 4가지 입력면 저장 비교
1010-drag-and-drop.md팔레트 → ResponsePrototype → Element 인스턴스화많음
1111-troubleshooting.md실전 문제: 로딩 실패, 폰트, 변수, a2w 직접 수정

빠른 참조#

# Script의 API 참조 — 실제로는 동작하지 않지만 구조는 이렇다
import alice
# 이름으로 접근 가능한 객체
bunny.color = alice.red
bunny.opacity = 0.5
# 애니메이션 함수
alice.MoveAnimation(subject=bunny, direction=alice.forward, amount=2, duration=2)
alice.SayAnimation(subject=bunny, what="Hello!")
# 제어 흐름
alice.DoInOrder(
alice.MoveAnimation(subject=bunny, amount=1, duration=1),
alice.TurnAnimation(subject=bunny, direction=alice.left, amount=0.25),
)
# 센서
if alice.keyboard.isKeyPressed(alice.keyboard.VK_SPACE):
print("Space pressed!")

주요 발견 사항 요약#

발견 사항근거상태
Jython 2.7.3 null cflags NPEInterpreter.java 56번째 줄 nullScript 전면 불가
HideAction = ShowActionanimations.py 702-703, 둘 다 isShowing=true버그
IfElseInOrder elseAnimation 무시animations.py 162-173, #todo로 남음미구현
LoopNInOrder = Noneanimations.py 184-185미구현
SoundAction 클래스로 저장 시 로드 실패s_classnameMap에 매핑 없음호환성 문제
Text3D / SayAnimation 한글 미지원Alice 2.6.1 폰트환경 제약

01 - 아키텍처 및 부트스트랩#

Alice 2가 Jython을 기동하고 World에 스크립팅 레이어를 연결하는 전체 과정. 분석 기준: alice2 소스 + Alice 2.6.1 배포본.


1. Jython 부트스트랩 순서#

sequenceDiagram
participant JVM as JVM
participant SF as ScriptingFactory<br/>(jython.ScriptingFactory)
participant JY as Jython 런타임
participant AT as 저작 도구
participant W as World
participant I as Interpreter
JVM->>SF: new ScriptingFactory()
SF->>JY: PySystemState.initialize(preProperties, ...)
Note over SF,JY: python.home = Required/jython/
SF->>JY: alice/__init__.py 읽기 및 컴파일
SF->>JY: Py.exec(code, builtins, builtins)
Note over SF,JY: alice.* 이름을 builtins에 주입<br/>(전체 alice를 모든 Interpreter가 공유)
AT->>W: new World()
AT->>W: setScriptingFactory(SF)
W->>SF: manufactureInterpreter()
SF-->>I: new Interpreter(this)
I->>I: new Namespace() + new PyModule("main", dict)
W->>I: setWorld(this)
I->>I: Namespace.setWorld(world) → clear() + setWorld()

주요 포인트#

  • alice/__init__.py가 builtins 네임스페이스에 로드된다 — 모든 alice.* 이름이 모든 인터프리터에서 바로 사용 가능
  • ScriptingFactory는 인터프리터 풀을 관리한다 (Vector<Interpreter>)
  • alice/__init__.pyanimations.py, constants.py, linearalgebra.py 전부 임포트
  • JVM 속성: -Dpython.home=jython -Dpython.path=jython/Lib/alice

2. World 레벨 실행 체인#

flowchart LR
E["Element.compile(script, source, type)"] --> EW["getWorld().compile(...)"]
EW --> WI["getInterpreter().compile(...)"]
WI --> PY["Py.compile_flags(script, pathname, CompileMode, flags)"]
PY --> JC["jython.Code(PyCode, CompileType)"]
E2["Element.eval(code)"] --> EW2["getWorld().eval(code)"]
EW2 --> WI2["getInterpreter().eval(code)"]
WI2 --> PYE["Py.eval(code.getPyCode(), ns, ns)"]
E3["Element.exec(code)"] --> EW3["getWorld().exec(code)"]
EW3 --> WI3["getInterpreter().exec(code)"]
WI3 --> PYX["Py.exec(code.getPyCode(), ns, ns)"]

실제 Java 구현#

Element.java
public Code compile(String script, Object source, CompileType compileType) {
return getWorld().compile(script, source, compileType);
}
public Object eval(Code code) {
return getWorld().eval(code);
}
public void exec(Code code) {
getWorld().exec(code);
}
// World.java
public Code compile(String script, Object source, CompileType compileType) {
return getInterpreter().compile(script, source, compileType);
}
public Object eval(Code code) {
return getInterpreter().eval(code);
}
public void exec(Code code) {
getInterpreter().exec(code);
}

3. CompileType 3종#

CompileTypeJython CompileMode특징사용처
EVALCompileMode.eval단일 표현식 평가, 결과 반환ScriptDefinedResponse
EXEC_SINGLECompileMode.single단일 구문 실행ScriptComboWidget (Go 버튼)
EXEC_MULTIPLECompileMode.exec여러 구문 실행ScriptResponse, Sandbox Script, ScratchPad

4. World.start() — 실제 코드로 보는 실행 흐름#

World.java
public void start() {
if (m_scriptingFactory != null) {
getInterpreter().start(); // Namespace 리셋
Code code = script.getCode(EXEC_MULTIPLE); // World.script Property → 컴파일
if (code != null) exec(code); // Sandbox Script 1회 실행
}
bubbles.clear();
started(this, m_clock.getTime());
m_isRunning = true;
}
public void schedule() {
m_currentSandbox = this;
scheduleBehaviors(m_clock.getTime()); // 매 프레임 behavior 실행
m_collisions = m_collisionManager.update(256);
m_currentSandbox = null;
}
sequenceDiagram
participant U as 유저가 Play 클릭
participant W as World
participant I as Interpreter
participant NS as Namespace
participant JB as Jython
participant WOBJ as Alice 객체들
U->>W: start()
W->>I: start()
I->>NS: resetNamespace() → clear() + setWorld()
W->>W: script.getCode(EXEC_MULTIPLE)
W->>I: exec(code)
I->>JB: Py.exec(pyCode, ns, ns)
JB->>NS: __finditem__("bunny")
NS->>NS: 로컬에도 없고, 월드 이름도 아님
NS->>WOBJ: m_pyWorld.__findattr__("bunny")
WOBJ-->>NS: PyElement(bunny)
NS-->>JB: PyElement
JB->>PyElement: bunny.color = red
PyElement->>WOBJ: property.set(red)
loop 매 프레임
W->>W: schedule()
W->>W: scheduleBehaviors(t)
Note over W: 모든 Sandbox behavior 실행
end

5. ScriptResponse와 ScriptDefinedResponse#

classDiagram
class Response {
+ScriptProperty script
+manufactureRuntimeResponse()
}
class ScriptResponse {
+update(t): exec(script, EXEC_MULTIPLE)
}
class ScriptDefinedResponse {
+prologue(t): eval(script, EVAL)
-m_actual: RuntimeResponse
+update(t): m_actual.update(t)
+getTimeRemaining(t): m_actual.getTimeRemaining(t)
}
class RuntimeResponse {
+prologue(t)
+update(t)
+epilogue(t)
}
Response <|-- ScriptResponse
Response <|-- ScriptDefinedResponse
ScriptDefinedResponse --> RuntimeResponse : m_actual에 위임

ScriptResponse (실제 코드)#

ScriptResponse.java
public class RuntimeScriptResponse extends RuntimeResponse {
public void update(double t) {
super.update(t);
ScriptResponse.this.exec(
ScriptResponse.this.script.getCode(CompileType.EXEC_MULTIPLE));
}
}

ScriptDefinedResponse (실제 코드)#

ScriptDefinedResponse.java
public void prologue(double t) {
super.prologue(t);
m_actual = null;
Object o = ScriptDefinedResponse.this.eval(
ScriptDefinedResponse.this.script.getCode(CompileType.EVAL));
if (o instanceof Response) {
m_actual = ((Response)o).manufactureRuntimeResponse();
if (m_actual != null) m_actual.prologue(t);
} else {
throw new RuntimeException("does not evaluate to a response: " + script);
}
}
public void update(double t) {
super.update(t);
if (m_actual != null) m_actual.update(t);
}
ScriptResponseScriptDefinedResponse
CompileTypeEXEC_MULTIPLEEVAL
실행 시기매 프레임 update(t)prologue(t)에서 1회
요구사항아무 Python 코드Response 객체 반환 필수
지속 시간0 (고정)위임받은 response에 따름
용도반복/연속 동작Python으로 애니메이션 동적 구성

ScriptDefinedResponse 예#

# 이 코드는 반드시 Response 객체를 반환해야 한다
alice.DoInOrder(
alice.MoveAnimation(subject=bunny, amount=1, duration=1),
alice.TurnAnimation(subject=bunny, direction=alice.left, amount=0.25),
)

6. ⚠️ 발견된 문제: Jython 2.7.3 null cflags (실제 코드 분석 결과)#

현상#

Alice 2.6.1에서 Script 기반 기능들이 런타임에 NPE로 크래시난다.

원인 (소스 코드 확인 완료)#

// Interpreter.java (jython/Interpreter.java, line 56)
public Code compile(String script, Object source, CompileType compileType) {
PyCode pyCode = Py.compile_flags(
script, source.toString(), compileType.getMode(),
null // ← Jython 2.7.3의 ParserFacade.prepBufReader()가 이 null을 못 받음
);
return new Code(pyCode, compileType);
}

참고: ScriptingFactory.java에서는 같은 API를 올바르게 호출함:

PyCode code = Py.compile_flags(script, pathname, CompileMode.exec,
Py.getCompilerFlags()); // ← flags를 제대로 전달

영향받는 기능#

기능결과
Sandbox Script (Edit Script)World.start() 시점에 NPE
ScriptResponse 블록update() 시점에 NPE
ScriptDefinedResponse 블록prologue() 시점에 NPE
ScriptComboWidget (Go)컴파일 시점에 NPE
ScriptScratchPad (Perform All)컴파일 시점에 NPE
드래그앤드롭 블록 전반✅ 정상 작동 (Script 미사용)

현실적 영향#

  • Script로 무언가를 하는 것은 Alice 2.6.1에서 불가능
  • alice/__init__.pyanimations.py에 정의된 Python API는 사용할 수 없음 (ScriptAction, MoveAnimation 등)
  • 블록 기반 편집만 가능

02 - 이름 해석: Python 이름 → Alice 객체#

스크립트에 bunny라고 쓰면 어떻게 Alice 월드의 오브젝트가 연결되는지 설명한다. ⚠️ 실제 코드 분석 기반 — PyElement.findattr_ex, PySandbox.setattr 전체 추적.


1. 전체 조회 체인#

Namespace.__finditem__(key)는 다음 순서로 이름을 찾는다:

flowchart TD
START["bunny.color = red"] --> NS["Namespace.__finditem__('bunny')"]
NS --> C1["1. local PyStringMap 확인<br/>(Python local/global var)"]
C1 -->|찾음| RET1["return PyObject"]
C1 -->|없음| C2["2. 월드 이름과 일치?"]
C2 -->|예| RETW["return m_pyWorld"]
C2 -->|아니오| C3["3. World.lookup(key)"]
C3 --> SB["Sandbox.lookup(key)"]
SB --> STK["a. currentBehavior.stackLookup(key)<br/>(파라미터, 지역 변수)"]
STK -->|찾음| REXPR["return Expression.getValue()"]
STK -->|없음| DTL["b. currentBehavior.detailLookup(key)<br/>(behavior 속성)"]
DTL -->|찾음| REXPR
DTL -->|없음| C4["4. m_pyWorld.__findattr__('bunny')"]
C4 --> PE["PyElement.__findattr_ex__('bunny')"]
PE --> CH["a. getChildNamedIgnoreCase('bunny')"]
CH -->|표현식 발견| CHVE["return expression.getValue()"]
CH -->|element 발견| CHVN["return PyElement(descendant)"]
CH -->|없음| PR["b. getPropertyNamedIgnoreCase('bunny')"]
PR -->|찾음| PRV["return property.get()"]
PR -->|없음| USCORE["c. name.startsWith('_')?"]
USCORE -->|예| RETRY["이름[1:]로 재시도<br/>child → property → reflection"]
USCORE -->|아니오| SUP["d. super.__findattr_ex__(name)<br/>(Java reflection fallback)"]

2. Java: Namespace.finditem (실제 코드)#

Namespace.java
public synchronized PyObject __finditem__(String key) {
// 1. Python local/global variables
PyObject py = super.__finditem__(key);
if (py != null) return py;
// 2. 월드 이름과 일치? → world 자체 반환
if (key.equalsIgnoreCase(m_world.name.getStringValue())) {
return m_pyWorld;
}
// 3. Sandbox.lookup() — behavior context (파라미터/지역변수)
Expression expression = m_world.lookup(key);
if (expression != null) {
return java2py(expression.getValue());
}
// 4. 최종 fallback: world의 child/property lookup
return m_pyWorld.__findattr__(key);
}

3. Sandbox.lookup() — Behavior Context (실제 코드)#

Sandbox.java
public Expression lookup(String key) {
if (m_currentBehavior != null) {
// a. behavior stack → parameters, local variables
Expression e = m_currentBehavior.stackLookup(key);
if (e != null) return e;
// b. behavior detail properties
return m_currentBehavior.detailLookup(key);
}
return null;
}

중요: 현재 실행 중인 Behavior의 지역변수/파라미터가 전역 오브젝트명보다 항상 우선한다. stackLookupdetailLookup 순서로 검색.


4. PyElement.findattr_ex — 속성 읽기 (실제 코드)#

PyElement.java
public PyObject __findattr_ex__(String name) {
// 1. Child Element 검색 (대소문자 구분 없음)
Element descendant = m_element.getChildNamedIgnoreCase(name);
if (descendant == null) {
// 2. Property 검색
Property property = m_element.getPropertyNamedIgnoreCase(name);
if (property == null) {
// 3. 언더스코어 prefix → child 건너뛰고 재시도
if (name.startsWith("_")) {
descendant = m_element.getChildNamed(name.substring(1));
if (descendant != null) return m_namespace.getPyElement(descendant);
property = m_element.getPropertyNamedIgnoreCase(name.substring(1));
if (property != null) return Py.java2py(property);
}
// 4. Java reflection fallback
return super.__findattr_ex__(name);
}
return m_namespace.java2py(property.get());
}
// Child가 Expression이면 getValue(), 아니면 PyElement로 래핑
Object value;
if (descendant instanceof Expression) {
value = ((Expression)descendant).getValue();
} else {
value = descendant;
}
return m_namespace.java2py(value);
}
sequenceDiagram
participant Script
participant NS as Namespace
participant PE as PyElement
participant Elem as Alice Element
Script->>NS: __finditem__("bunny")
NS->>PE: __findattr_ex__("bunny")
PE->>Elem: getChildNamedIgnoreCase("bunny")
Elem-->>PE: Element(bunny Transformable)
Note over PE: descendant is not Expression
PE-->>NS: PyElement(bunny)
NS-->>Script: PyElement(bunny)
Script->>PE: bunny.color (읽기)
PE->>Elem: getPropertyNamedIgnoreCase("color")
Elem-->>PE: Color property
PE->>PE: property.get() -> Color.RED
PE-->>Script: PyColor(RED)
Script->>PE: bunny.color = alice.red (쓰기)
PE->>Elem: getPropertyNamedIgnoreCase("color")
Elem-->>PE: Color property
PE->>Elem: property.set(attr.__tojava__(Color.class))

5. ⭐ PySandbox.setattr — 변수에 값 쓰기 (실제 코드)#

PySandbox.java
public void __setattr__(String name, PyObject attr) {
// 우선 Sandbox.variables 배열에서 이름이 일치하는 변수 찾기
for (int i = 0; i < getSandbox().variables.size(); i++) {
Variable variable = (Variable)getSandbox().variables.get(i);
if (name.equalsIgnoreCase(variable.name.getStringValue())) {
// Alice UI 변수에 직접 값 설정!
variable.value.set(attr.__tojava__(variable.getValueClass()));
return;
}
}
// 없으면 일반 PyElement.__setattr__ 위임
super.__setattr__(name, attr);
}

이것이 의미하는 것#

# Alice UI에서 만든 전역 변수 score가 있을 때:
score = 5 # ← PySandbox.__setattr__가 가로챔!
# ← Sandbox.variables 중 "score"를 찾아 value.set(5)
# ← Python dict에 "score"가 생기지 않음!
# Alice UI 변수가 없으면:
my_python_var = 42 # ← 일반 Python 변수 (Namespace dict에 저장)

유저 입장: Alice UI 변수는 할당으로 바로 값 변경 가능하다.

score = score + 1 # ✅ Alice 전역 변수 증가

6. PyElement.setattr — Property 쓰기 (실제 코드)#

PyElement.java
public void __setattr__(String name, PyObject attr) {
Property property = m_element.getPropertyNamedIgnoreCase(name);
if (property != null) {
// Python → Java 타입 변환 후 Property.set()
property.set(attr.__tojava__(property.getValueClass()));
} else {
super.__setattr__(name, attr); // Python instance attribute
}
}

7. 특수 구문#

구문의미예제
__UnnamedN__인덱스로 이름 없는 child 접근__Unnamed0__
/ 구분자계층적 경로 (internalGetDescendantKeyed)world.bunny.leftLeg
_name prefixchild 조회 건너뛰고 바로 property로_color (child가 있어도 property로)

8. 전체 추적: bunny.color = alice.red#

1. Jython이 "bunny.color = alice.red" 파싱
→ bunny 평가: Namespace.__finditem__("bunny")
2. Namespace.__finditem__("bunny")
→ local dict: 없음
→ world name: "world" 아님
→ World.lookup("bunny"): m_currentBehavior == null → null 반환
→ m_pyWorld.__findattr__("bunny")
3. PyElement.__findattr_ex__("bunny")
→ getChildNamedIgnoreCase("bunny") → Element(bunny)
→ descendant is not Expression
→ return PyElement(bunny)
4. Python: bunny.color = alice.red
→ PyElement.__setattr__("color", red_pyobj)
5. PyElement.__setattr__("color", PyObject(alice.red))
→ getPropertyNamedIgnoreCase("color") → ColorProperty
→ attr.__tojava__(Color.class) → Java Color.RED
→ property.set(Color.RED)
6. "alice.red" 해석:
from edu.cmu.cs.stage3.alice.scenegraph import Color
red = Color.RED # Java static field from constants.py

9. ⭐ 키 포인트 요약#

  1. 이름은 대소문자를 구분하지 않는다 (getChildNamedIgnoreCase, equalsIgnoreCase)
  2. Behavior의 지역변수 > 전역 오브젝트명 (stackLookup 우선)
  3. Alice UI 변수 = Python 할당 가능 (PySandbox.__setattr__가 가로챔)
  4. _name prefix = child 우회 (같은 이름의 child와 property가 있을 때 유용)
  5. __UnnamedN__ = 이름 없는 child 접근 (DefinedResponse 내부 구조 분석용)
  6. / 경로 = 계층적 접근 (world.bunny.leftLeg 가능)

03 - 스크립트 입력면: 코드를 작성하는 4가지 방법#

Alice 2에서 스크립트를 입력하고 실행하는 4가지 방법을 유저 입장에서 설명한다. 각 입력면마다: UI 위치, 조작 단계, 실행되는 시점, 실제 예제, 주의사항. 분석 기준: alice2 source + Alice 2.6.1 배포본.


개요#

flowchart TD
ALICE["Alice 2 실행"] -->|world 선택| CM["오른쪽 클릭"]
CM -->|enableScripting=true| ES["Edit Script 메뉴"]
ES --> SES["ScriptEditor 창<br/>여러 줄 코드 입력"]
SES -->|"World.start()시"| EXEC1["EXEC_MULTIPLE 1회 실행"]
ALICE -->|ResponseEditor| PALL["블록 팔레트"]
PALL -->|드래그| SR["ScriptResponse 블록"]
SR --> SRIN["script 필드에 코드 입력"]
SRIN -->|매 프레임| EXEC2["EXEC_MULTIPLE 실행"]
PALL -->|드래그| SDR["ScriptDefinedResponse 블록"]
SDR --> SDRIN["Response 반환 표현식 입력"]
SDRIN -->|프롤로그 1회| EVAL["EVAL"]
ALICE -->|하단 UI| CW["ComboWidget + ScratchPad"]
CW -->|한 줄 입력 + Go| EXEC3["EXEC_SINGLE 즉시 실행"]
CW -->|여러 줄 + Perform All| EXEC4["EXEC_MULTIPLE 즉시 실행"]

🚨 실행 불가 확인 (Alice 2.6.1)#

소스 코드 분석 결과: Alice 2.6.1에서 모든 Script 기능이 Jython 2.7.3의 Py.compile_flags null cflags 문제로 런타임에 동작하지 않는다.

  • 관련 파일: Interpreter.java 56번째 줄 — Py.compile_flags(script, source, mode, null) 마지막 인자가 null
  • ScriptingFactory.java에서 builtins 로딩할 때는 Py.getCompilerFlags()를 제대로 사용
입력면시점결과
Sandbox ScriptWorld.start() 시NPE
ScriptResponse매 프레임 update()NPE
ScriptDefinedResponseprologue() 시NPE
ScriptComboWidget컴파일 시NPE
ScriptScratchPad컴파일 시NPE

블록 기반 편집은 정상 작동한다.


입력면 1: Sandbox Script (Edit Script)#

UI 위치와 조작#

1. Alice 2 실행, 월드 선택
2. 월드 또는 샌드박스 오브젝트에서 오른쪽 클릭
3. 컨텍스트 메뉴에서 "edit script" 선택
4. ScriptEditor 창이 열림 (줄 번호 표시 + 일반 텍스트 편집기)
5. Python 코드 입력

참고: 이 메뉴는 enableScripting = true 설정일 때만 나타난다.

편집 대상 (소스 코드 확인)#

// ElementPopupUtilities.java — EditScriptRunnable
public void run() {
if (element instanceof Sandbox) {
authoringTool.editObject(((Sandbox)element).script);
}
}

편집 대상 = Sandbox.script (ScriptProperty). ZIP 내 /script.py에 저장됨.

실행 시점 (소스 코드 확인)#

// World.java start()
public void start() {
if (m_scriptingFactory != null) {
getInterpreter().start();
Code code = script.getCode(EXEC_MULTIPLE); // World.script Property
if (code != null) exec(code); // Sandbox Script 1회 실행
}
// ...
}
sequenceDiagram
participant User
participant World
participant Interp as Interpreter
participant Sandbox
User->>World: Play 버튼 클릭
World->>Interp: start()
Interp->>Interp: Namespace.reset() → clear() + setWorld()
World->>World: script.getCode(EXEC_MULTIPLE)
World->>Interp: exec(code)
Note over Interp: Sandbox Script **한 번** 실행
World->>World: schedule()
loop 매 프레임
World->>World: scheduleBehaviors(t)
World->>Sandbox: sandbox.scheduleBehaviors(t)
end

입력면 2: ScriptResponse 블록#

UI 위치와 조작#

1. ResponseEditor 열기 (오브젝트 더블클릭 또는 편집 메뉴)
2. 좌측 블록 팔레트에서 "script" 라벨이 붙은 블록 찾기
3. 블록을 드래그하여 DoInOrder 등의 ContainerResponse에 드롭
4. 생성된 ScriptResponse 블록을 클릭
5. 속성 패널에 "script" 텍스트 필드가 나타남
6. 여기에 Python 코드 입력

실행 방식 (소스 코드)#

ScriptResponse.java
public class RuntimeScriptResponse extends RuntimeResponse {
public void update(double t) {
super.update(t);
ScriptResponse.this.exec(
ScriptResponse.this.script.getCode(CompileType.EXEC_MULTIPLE));
}
}
sequenceDiagram
participant Sched as 스케줄러
participant Resp as ScriptResponse
participant ScriptProp as ScriptProperty
participant Int as Interpreter
Sched->>Resp: update(t)
Resp->>ScriptProp: getCode(EXEC_MULTIPLE)
ScriptProp->>ScriptProp: 캐시 미스? compile()
ScriptProp-->>Resp: Code (캐시됨)
Resp->>Int: exec(code)
Note over Resp,Int: 매 프레임마다 실행됨!
Int-->>Resp: 완료
Sched->>Resp: 다음 update(t+dt) ...

입력면 3: ScriptDefinedResponse 블록#

UI 위치와 조작#

1. ResponseEditor 열기
2. 팔레트에서 "script defined" 라벨 블록 찾기
3. ContainerResponse에 드롭
4. 블록 클릭 → "script" 속성 필드
5. 여기에 Response 객체를 반환하는 Python 표현식 입력

실행 방식 (소스 코드)#

ScriptDefinedResponse.java
public void prologue(double t) {
super.prologue(t);
m_actual = null;
Object o = ScriptDefinedResponse.this.eval(
ScriptDefinedResponse.this.script.getCode(CompileType.EVAL));
if (o instanceof Response) {
m_actual = ((Response)o).manufactureRuntimeResponse();
if (m_actual != null) m_actual.prologue(t);
} else {
throw new RuntimeException("does not evaluate to a response: " + script);
}
}
sequenceDiagram
participant Sched as 스케줄러
participant SDR as ScriptDefinedResponse
participant Int as Interpreter
participant Deleg as 위임받은 RuntimeResponse
Sched->>SDR: prologue(t0)
SDR->>Int: eval(code) -- EVAL 모드
Int-->>SDR: Response 객체 (예: DoInOrder)
SDR->>Deleg: manufactureRuntimeResponse()
SDR->>Deleg: prologue(t0)
loop 매 프레임
Sched->>SDR: update(t)
SDR->>Deleg: update(t)
end
Sched->>SDR: epilogue(tN)
SDR->>Deleg: epilogue(tN)

입력면 4: ScriptComboWidget + ScriptScratchPad#

UI 위치#

Alice 하단 툴바 영역:

┌──────────────────────────────────────────────────────┐
│ [ 입력 + 히스토리(콤보박스) ] [ Go 버튼 ] │ ← ScriptComboWidget
├──────────────────────────────────────────────────────┤
│ [ Perform All (Ctrl+F4) ] [ Perform Selected (F4) ] │ ← ScriptScratchPad 버튼
├──────────────────────────────────────────────────────┤
│ Scratch Pad (임시 편집기) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 여러 줄 입력 가능 │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘

입력면 4a: ScriptComboWidget (Go)#

ScriptComboWidget.java
public void runScript() {
String script = ((String)comboBox.getEditor().getItem()).trim();
if (script.length() != 0) {
Code code = sandbox.compile(script, "<Run Line>", EXEC_SINGLE);
sandbox.exec(code);
comboBox.insertItemAt(script, 0);
}
}
  • 한 줄 입력 → Go 또는 Enter → EXEC_SINGLE
  • 실행 후 히스토리에 저장 (재선택 가능)
  • 저장되지 않음 (휘발성)

입력면 4b: ScriptScratchPad (Perform All / Perform Selected)#

ScriptEditorPane.java
// Perform All (Ctrl+F4)
script = ScriptEditorPane.this.getText();
code = sandbox.compile(script, "<ScriptEditorPane>", EXEC_MULTIPLE);
sandbox.exec(code);
// Perform Selected (F4)
int selStart = getLineStartOffset(getLineOfOffset(getSelectionStart()));
int selEnd = getLineEndOffset(getLineOfOffset(getSelectionEnd()));
script = getText(selStart, selEnd - selStart);
code = sandbox.compile(script, "<ScriptEditorPane>", EXEC_MULTIPLE);
sandbox.exec(code);
  • 저장되지 않음 (휘발성)
  • 영구 저장 필요 시 Sandbox Script 또는 ScriptResponse 블록으로 이전 필요

입력면 5: ResponseEditor 팔레트 블록#

팔레트 블록Response 클래스스크립트 필요?
Do in orderDoInOrder하위 블록 필요
Do togetherDoTogether하위 블록 필요
Loop (N times)LoopNInOrderend/increment
If/ElseIfElseInOrdercondition
WhileWhileLoopInOrdercondition
For each in orderForEachInOrderlist
For all togetherForEachTogetherlist
ScriptScriptResponsescript 필드
Script definedScriptDefinedResponsescript 필드
WaitWaitduration
CommentComment(실행 안 됨)
PrintPrinttext

저장/휘발 구분#

편집 위치a2w 내 저장 위치지속성
Edit Script/script.py✅ 저장
ScriptResponse 블록/.../script.py✅ 저장
ScriptDefinedResponse 블록/.../script.py✅ 저장
ScriptComboWidget메모리❌ 휘발
ScriptScratchPad메모리❌ 휘발

04 - API 참조: alice Python 패키지#

Required/jython/Lib/alice/ 아래 4개 파일이 제공하는 모든 함수와 상수. 분석 기준: Alice 2.6.1 배포본의 실제 .py 파일.


패키지 구조#

Required/jython/Lib/alice/
__init__.py - 임포트, Keyboard, Mouse, Clock, copy(), exportToObj()
animations.py - 모든 애니메이션 + 제어 흐름 함수
constants.py - 색상, 방향, 스타일, HowMuch
linearalgebra.py - Vec3, dot(), cross()
pinchgloves.py - Pinch Gloves 하드웨어 연동 (직렬 COM 포트)

상수 (constants.py)#

방향#

NameJava Source
leftDirection.LEFT
rightDirection.RIGHT
upDirection.UP
downDirection.DOWN
forwardDirection.FORWARD
back / backwardDirection.BACKWARD

차원#

NameJava Source
leftToRightDimension.LEFT_TO_RIGHT
topToBottomDimension.TOP_TO_BOTTOM
frontToBackDimension.FRONT_TO_BACK
allDimension.ALL

HowMuch (실행 범위)#

Name의미
instance이 Element만
instanceAndPartsElement + parts
instanceAndAllDescendantsElement + 모든 하위 요소

애니메이션 스타일 (이징)#

NameJava Source설명
abruptlyLINEAR이징 없음
beginGentlySLOW_IN시작만 느리게
endGentlySLOW_OUT끝만 느리게
gentlySLOW_IN_OUT시작과 끝 모두 느리게 (기본값)

색상#

edu.cmu.cs.stage3.alice.scenegraph.Color의 static 필드:

Color
redpinkorangeyellow
greenbluepurplebrown
whitelightGraygraydarkGray
blackcyanmagenta

채우기 / 쉐이딩 스타일#

Name설명
solid / wireframe / points채우기 스타일
noShading / flatShading / smoothShading쉐이딩 스타일

공간 관계#

leftOf | rightOf | above | below | inFrontOf | behind


선형대수 (linearalgebra.py)#

v = alice.Vec3(1, 2, 3)
v2 = alice.Vec3(4, 5, 6)
v + v2 # Vec3(5, 7, 9)
v * 2 # Vec3(2, 4, 6)
v.getLength() # 3.74
alice.dot(v, v2) # 32
alice.cross(v, v2) # Vec3(-3, 6, -3)

센서 (__init__.py)#

키보드#

kb = alice.keyboard
kb.isKeyPressed(kb.VK_SPACE) # 현재 키 상태
kb.getKeyNames() # 키 이름 목록

모든 java.awt.event.KeyEvent.VK_* 상수 사용 가능: VK_SPACE, VK_ENTER, VK_AVK_Z, VK_0VK_9

마우스#

pos = alice.mouse.getLocation() # → java.awt.Point

시계#

t = alice.clock.getTime() # → System.currentTimeMillis / 1000 (초)

애니메이션 함수 (animations.py — 실제 코드 기반)#

이동#

함수설명
MoveAnimation(subject, direction, amount, duration, asSeenBy, style, isScaledBySize)이동
TurnAnimation(subject, direction, amount, duration, ...)회전 (앞/뒤/좌/우)
RollAnimation(subject, direction, amount, ...)굴림 (좌/우)
ResizeAnimation(subject, dimension, amount, likeRubber, ...)크기 조절
PositionAnimation(subject, position, ...)절대 위치 설정
QuaternionAnimation(subject, quaternion, ...)절대 회전 (쿼터니언)
EulerAnglesAnimation(subject, pitchYawRoll, ...)절대 회전 (오일러)
SizeAnimation(subject, size, ...)절대 크기 설정
PointAtAnimation(subject, target, offset, upGuide, onlyAffectYaw, ...)타겟 지향
PointOfViewAnimation(subject, pointOfView, ...)전체 자세 설정
StandUpAnimation(subject, ...)월드 업 정렬
PathAnimation(subject, pointOfViews, times, ...)키프레임 경로
ForwardVectorAnimation(subject, forward, upGuide, ...)전방 벡터 설정

텍스처#

함수설명
TextureMapAnimation(subject, textureMaps, framesPerSecond, ...)텍스처 플립북

속성 및 차량#

함수설명
PropertyAnimation(element, propertyName, value, ...)모든 Property를 시간에 따라 애니메이션
VehiclePropertyAnimation(element, vehicle)Vehicle 즉시 설정 (duration=0)

소리 및 표시#

함수설명
SoundAction(sound, volumeLevel, wait)소리 재생 (내부적으로 SoundResponse 사용)
ShowAction(element, howMuch, wait)isShowing = true
HideAction(element, howMuch, wait)⚠️ 실제로는 isShowing = true (소스 확인 완료)
SayAnimation(subject, what, ...)말풍선
ThinkAnimation(subject, what, ...)생각풍선

제어 흐름#

함수설명
DoInOrder(*animations, wait)순차 실행
DoTogether(*animations, wait)병렬 실행
ForEachInOrder(each, list, *animations, wait)순차 반복
ForEachTogether(each, list, *animations, wait)병렬 반복
IfElseInOrder(condition, ifAnimation, elseAnimation)⚠️ elseAnimation 미구현 (소스 확인 완료)
WhileLoopInOrder(condition, *animations)While 루프
LoopNInOrder(...)None 반환 (미구현)
WaitAction(duration)대기
ScriptAction(fcn_or_string, args, kwargs)Python 함수를 Response로 래핑

기본 파라미터 패턴#

def AnimFunc(subject, ..., duration=1, style=gently, wait=0):
  • 모든 파라미터에 callable 전달 가능 (ApplyQuestion으로 자동 래핑)
  • wait=1이면 DoInOrder 내에서 블로킹
  • subject, direction, amount 등 전부 callable 지원

⚠️ animations.py에서 발견된 버그 (실제 코드 분석)#

버그 1: HideAction = ShowAction#

# animations.py 699-703
def ShowAction(element, howMuch=instanceAndParts, wait=0):
return PropertyAnimation(element, "isShowing", true, duration=0, ...)
def HideAction(element, howMuch=instanceAndParts, wait=0):
return PropertyAnimation(element, "isShowing", true, duration=0, ...) # ← false여야 함

두 함수가 완전히 동일. HideAction을 호출해도 isShowing = true로 설정됨.

버그 2: IfElseInOrder가 elseAnimation 미처리#

# animations.py 162-173
def IfElseInOrder(condition, ifAnimation, elseAnimation=None):
a = IfElseInOrder()
a.addComponentResponse(ifAnimation)
#todo ← elseAnimation 파라미터를 전혀 사용하지 않음
...

elseAnimation 파라미터는 받지만 내부적으로 무시됨.

버그 3: LoopNInOrder = None#

# animations.py 184-185
def LoopNInOrder(*args):
return None

구현 자체가 없음.


고급: ScriptAction#

def ScriptAction(fcn, args=(), kws={}):
if type(fcn) == str:
# 문자열 → ScriptResponse (매 프레임 실행)
sr = ScriptResponse()
sr.script.set(fcn)
return sr
else:
# Callable → ApplyResponse (1회 호출)
return ApplyResponse(fcn, args, kws)

고급: ApplyQuestion (동적 파라미터)#

모든 animation 함수는 callable 파라미터를 자동으로 ApplyQuestion으로 래핑:

alice.MoveAnimation(
subject=bunny,
amount=lambda: alice.random.random() * 5,
)

유틸리티 함수#

alice.copy(src, name="bunny2") # 깊은 복사
alice.exportToObj(model, "bunny.obj") # OBJ 내보내기
alice.ManufactureEach([bunny, snowman]) # ForEach 용 Variable

Pinch Gloves (pinchgloves.py)#

import pinchgloves
pinchgloves.AddPinchResponse(
(pinchgloves.LEFT_THUMB, pinchgloves.LEFT_INDEX),
on_pinch_callback, on_release_callback,
)
pinchgloves.SetDefaultPort("COM1")
pinchgloves.Start()

05 - 내부 구조: Java 클래스 참조#

Alice 2의 Jython 스크립팅 시스템을 구성하는 Java 클래스 전체 맵. 분석 기준: alice2 source 전체 트레이스.


클래스 계층#

classDiagram
class ScriptingFactory {
<<interface>>
+manufactureInterpreter()
+getInterpreters()
+setStdOut(OutputStream)
+setStdErr(OutputStream)
}
class Interpreter {
<<interface>>
+setWorld(World)
+start()/stop()
+compile(String, Object, CompileType) Code
+eval(Code) Object
+exec(Code)
}
class Code {
<<interface>>
+getCompileType() CompileType
}
class CompileType {
<<interface>>
+EVAL / EXEC_SINGLE / EXEC_MULTIPLE
+getMode() CompileMode
}
class jython_ScriptingFactory {
+ScriptingFactory()
-m_interpreters: Vector
}
class jython_Interpreter {
-m_module: PyModule
-m_dict: Namespace
-m_world: World
}
class jython_Code {
-m_pyCode: PyCode
-m_compileType: CompileType
}
class Namespace {
-m_world: World
-m_pyWorld: PyElement
-m_map: Hashtable
+__finditem__(String) PyObject
+java2py(Object) PyObject
}
class PyElement {
-m_element: Element
-m_namespace: Namespace
+__findattr_ex__(String) PyObject
+__setattr__(String, PyObject)
}
class PySandbox {
+__setattr__(String, PyObject)
}
ScriptingFactory <|.. jython_ScriptingFactory
Interpreter <|.. jython_Interpreter
Code <|.. jython_Code
jython_Interpreter --> Namespace : 소유
Namespace --> PyElement : 생성
PyElement <|-- PySandbox

Java 파일 인벤토리#

Scripting API (인터페이스)#

경로: src/main/java/edu/cmu/cs/stage3/alice/scripting/

파일역할
ScriptingFactory.java인터프리터 생성/해제, I/O 리디렉션
Interpreter.javacompile, eval, exec, start, stop
Code.java컴파일된 스크립트 핸들
CompileType.javaEVAL / EXEC_SINGLE / EXEC_MULTIPLE

Jython 구현#

경로: src/main/java/edu/cmu/cs/stage3/alice/scripting/jython/

파일역할특징
ScriptingFactory.javaJython 초기화: PySystemState.initialize, alice/init.py 로드builtins에 로드
Interpreter.javacompile: Py.compile_flags → 마지막 인자 null⚠️ NPE 원인
Code.javaPyCode + CompileType 래핑
Namespace.javaPyStringMap 상속, 4단계 이름 해석java2py로 Element → PyElement 변환
PyElement.javaPyInstance 상속, findattr_ex 4단계child → property → _prefix → reflection
PySandbox.javaPyElement 상속, setattr 오버라이드변수명 일치 시 variable.value.set()

Alice Core 통합#

경로: src/main/java/edu/cmu/cs/stage3/alice/core/

파일역할
World.javaScriptingFactory 소유, compile/eval/exec 위임, start()에서 script 1회 실행
Element.javacompile/eval/exec을 getWorld()로 위임, getChildNamedIgnoreCase / getPropertyNamedIgnoreCase
Sandbox.javaScriptProperty 보유, behavior 스케줄, lookup() 이름 해석

경로: src/main/java/edu/cmu/cs/stage3/alice/core/property/

파일역할
ScriptProperty.javaStringProperty 상속, Code 캐싱, .py 파일 저장/로드

경로: src/main/java/edu/cmu/cs/stage3/alice/core/responses/

파일역할
ScriptResponse.javaupdate(t) → exec(EXEC_MULTIPLE) 매 프레임
ScriptDefinedResponse.javaprologue(t) → eval(EVAL), 위임 패턴

저작 도구 (UI)#

경로: src/main/java/edu/cmu/cs/stage3/alice/authoringtool/

파일역할
editors/scripteditor/ScriptEditor.javaScriptEditor 전체
util/ScriptEditorPane.javaPerform All/Selected, undo/redo
util/ScriptScratchPad.java임시 메모장
util/ScriptComboWidget.java한 줄 실행 콤보박스
util/ElementPopupUtilities.javaEditScriptRunnable

Element.s_classnameMap (전체 목록)#

Element.java static initializer에서 확인된 클래스명 매핑 (이전 버전 → 현재):

// 이름이 바뀐 Response 클래스들
ConditionalLoopSequentialResponse → WhileLoopInOrder
ConditionalSequentialResponse → IfElseInOrder
CountLoopSequentialResponse → LoopNInOrder
ForEachInListSequentialResponse → ForEach
OrientationAnimation → ForwardVectorAnimation
ParallelForEachInListSequentialResponse → ForEachTogether
ParallelResponse → DoTogether
ProxyForScriptDefinedResponse → ScriptDefinedResponse
SequentialForEachInListSequentialResponse → ForEachInOrder
SequentialResponse → DoInOrder
MetaResponse → CallToUserDefinedResponse
// 내비게이션
KeyboardNavigationBehavior → KeyboardNavigationBehavior
MouseNavigationBehavior → MouseLookingBehavior
// 포즈
Pose → Pose
PoseAnimation → PoseAnimation
KeyMapping → KeyMapping

⚠️ 없는 매핑 (참고)#

edu.cmu.cs.stage3.alice.core.responses.SoundAction → ??? (없음)

a2w 파일에 SoundAction 클래스명으로 저장된 Response가 있으면 로드 실패. 직접 ZIP을 열어 elementData.xml을 수정하거나 Alice UI에서 블록을 다시 설정해야 함.


코드 실행 체인 요약#

블록/Python 코드 → ScriptProperty.getCode()
→ owner.compile(script, owner, compileType)
→ World.compile()
→ Interpreter.compile()
→ Py.compile_flags(script, path, mode, flags) → PyCode
→ new Code(PyCode, CompileType)
→ Code 반환
→ Code 캐싱 (m_code)
→ exec(code) / eval(code)
→ World.exec() → Interpreter.exec()
→ Py.exec(pyCode, ns, ns) / __builtin__.eval(pyCode, ns, ns)

Alice 2 - a2w 파일 포맷 (World)#

대상: Alice 2 World 파일 (.a2w)의 내부 구조와 저장/로드 메커니즘 출처: Element.java, AuthoringTool.java, ZipTreeStorer.java, ZipFileTreeStorer.java 마지막 업데이트: 2026-06-09


1. ZIP Archive 개요#

a2w 파일은 ZIP archive다. 매직 바이트 PK\x03\x04로 시작하며, 기본적으로 Deflate 압축.

구분
매직 시그니처50 4B 03 04 (PK\x03\x04)
기본 압축Deflate (java.util.zip)
로더 클래스ZipTreeLoader / ZipFileTreeLoader
스토러 클래스ZipTreeStorer (순차) / ZipFileTreeStorer (in-place)
World 확장자.a2w

2. a2w ZIP 내부 파일 목록#

실제 default_English.a2w 분석 결과:

flowchart TD
subgraph ZIP["world.a2w (ZIP Archive)"]
EH["elementCountHint.txt - 진행률 힌트"]
TN["thumbnail.png - 120x90 미리보기"]
SP["script.py - World Sandbox script"]
EX["elementData.xml - 루트 World 직렬화"]
CAM["camera/"]
LIT["light/"]
GRD["ground/ + __ita__/ + texture/"]
BEH["behavior0/ + __Unnamed0__/ + __Unnamed1__/"]
MET["my first method/"]
end
EH --> LOAD["Element.load()"]
SP --- EX
EX --> CAM & LIT & GRD & BEH & MET
style SP fill:#ff0,stroke:#a80
style EX fill:#aef,stroke:#08a

elementCountHint.txt#

로딩 프로그레스바용 힌트. 한 줄 정수.

  • 없어도 로딩에 지장 없음 (FileNotFoundException 무시)

thumbnail.png#

120x90 PNG. 조건부 저장 (config.saveThumbnailWithWorld).

script.py#

World의 Sandbox.script 프로퍼티의 외부 파일.

  • 저장: ScriptProperty.encodeObject() > script.py 생성
  • 로드: ScriptProperty.decodeObject() > script.py 읽어 set()
  • 자세한 내용: 09-storing-scripts.md

elementData.xml#

각 Element 하나당 하나씩 존재하는 XML 직렬화.

하위 디렉토리 규칙#

종류디렉토리명
named childchild.namecamera, my first method
unnamed child__UnnamedN__ (0-indexed)__Unnamed0__, __Unnamed1__

3. World 저장 (Store) Flow#

sequenceDiagram
participant User as 유저
participant AT as AuthoringTool
participant ST as ZipFileTreeStorer
participant EL as World (Element)
participant SP as ScriptProperty
User->>AT: Ctrl+S / Save World
AT->>AT: saveWorldToFile(file)
AT->>AT: fireWorldSaving, saveTabs, saveCount
AT->>AT: thumbnail 생성 (설정 시)
AT->>ST: open(file) / parse central dir
ST->>EL: world.store(storer, observer, map)
EL->>EL: elementCountHint.txt 쓰기
EL->>EL: thumbnail.png 등 map 쓰기
EL->>EL: internalStore() 재귀
loop 각 child
EL->>ST: createDir + setCurrentDir
EL->>EL: child.internalStore() 재귀
end
SP->>ST: getKeepKey -> create or keep script.py
EL->>ST: write elementData.xml
AT->>ST: close() - delete, reorder, write CD

4. World 로드 (Load) Flow#

sequenceDiagram
participant AT as AuthoringTool
participant LD as ZipFileTreeLoader
participant EL as Element
participant SP as ScriptProperty
AT->>LD: new ZipFileTreeLoader().open(file)
LD->>LD: read all entries to memory
AT->>EL: Element.load(loader, externalRoot, observer)
EL->>EL: read elementCountHint.txt
EL->>EL: parse elementData.xml -> Class.forName -> instantiate
SP->>SP: decodeObject -> read script.py -> set()
loop 각 <child>
EL->>EL: setCurrentDir -> recursive load -> addChild
end
EL->>EL: ReferenceResolver.resolve()
AT-->>User: World loaded

5. ZIP 스토러 비교#

특성ZipTreeStorerZipFileTreeStorer
쓰기 방식순차 ZipOutputStreamRandomAccessFile
in-place
getKeepKeynull파일 기반
사용처a2c (갤러리)a2w (World)

6. 주요 포인트#

  • a2w = Element 트리 -> ZIP 디렉토리 1:1 매핑
  • 각 Element = elementData.xml 하나
  • 스크립트 = ScriptProperty에 의해 별도 .py 파일로 분리 저장
  • Keep file 메커니즘으로 변경된 부분만 재기록
  • 로드/저장 모두 재귀적 (internalStore() / 재귀 load())

Alice 2 - a2c 파일 포맷 (캐릭터/갤러리 오브젝트)#

대상: Alice 2 Character/Gallery Object 파일 (.a2c)의 내부 구조 출처: TheAliceProject/alice2 - LocalGalleryObject.java, GalleryViewer.java 마지막 업데이트: 2026-06-09


1. a2c 개요#

a2c 파일도 a2w와 동일하게 ZIP archive다. 차이점은:

  • 용도: 갤러리에 표시되는 캐릭터/오브젝트/씬 조각
  • 편집 가능?: a2c는 읽기 전용 (갤러리에서 드래그하여 월드로 가져오기만 가능)
  • 저장 방식: ZipTreeStorer (순차 쓰기, 새 파일 생성)
  • 필수 파일: elementData.xml, galleryData.xml, thumbnail.png

2. a2c ZIP 내부 구조#

아래는 실제 BumperCars.a2c의 구조다.

flowchart TD
subgraph ZIP["Bopper.a2c (ZIP Archive)"]
EH["elementCountHint.txt\n3\n"]
GD["galleryData.xml\n⭐ 갤러리 메타데이터"]
TN["thumbnail.png\n갤러리 썸네일"]
EX["elementData.xml\nModel (Bopper) 직렬화"]
ITA["__ita__/\n ├ elementData.xml\n ├ indices.bin\n └ vertices.bin"]
MAT["Material #2_CL/\n ├ elementData.xml\n └ image.png"]
end
GD -->|갤러리 표시용| GV[GalleryViewer]
EX -->|오브젝트 데이터| LOAD["Element.load()"]
style GD fill:#fda,stroke:#a60

3. galleryData.xml#

a2w와의 가장 큰 차이점은 galleryData.xml이 존재한다는 것이다.

<?xml version="1.0" encoding="UTF-8"?>
<model>
<name>bumperCars</name>
<parts>3</parts>
<physicalsize>29.95m x 6.2m x 31.29m</physicalsize>
<methods/>
<questions/>
<sounds/>
</model>
태그의미
<name>갤러리에 표시할 이름
<parts>구성 파트 수 (계층에서 분리 가능한 자식 Model 수)
<physicalsize>물리적 크기 (미터 단위)
<methods>노출할 메서드 목록
<questions>노출할 질문(Question) 목록
<sounds>포함된 사운드 목록

갤러리에서 a2c를 드래그하여 월드에 놓으면, Alice는 elementData.xml을 로드하여 Model 인스턴스를 생성하고, galleryData.xml의 정보는 GalleryViewer UI에 표시되는 용도로만 사용된다.


4. elementData.xml (a2c 버전)#

a2c의 루트 Element는 World가 아니라 Model 이다.

<?xml version="1.0" encoding="UTF-8"?>
<element class="edu.cmu.cs.stage3.alice.core.Model"
modelName="Bopper" name="bopper" version="2.2">
<child filename="__ita__"/>
<child filename="Material #2_CL"/>
<property name="color"><red>1.0</red><green>1.0</green><blue>1.0</blue><alpha>1.0</alpha></property>
<property class="java.lang.Double" name="opacity">1.0</property>
</element>
특징a2w (World)a2c (Character)
루트 클래스WorldModel
galleryData.xml없음있음
script.py있음 (빈 파일)없음
thumbnail.png옵션 (설정)필수
저장 시 스토러ZipFileTreeStorer (in-place)ZipTreeStorer (순차)
편집 가능

5. a2c가 갤러리에서 동작하는 방식#

  1. Alice 실행 > GalleryViewerRequired/gallery/ 디렉토리 스캔
  2. .a2c 파일에 대해 LocalGalleryObject 생성
  3. thumbnail.png - 썸네일, galleryData.xml - 이름/크기/파트 수 표시
  4. 사용자가 드래그하여 월드에 가져올 때: Element.load(a2cFile, externalRoot) > Model 인스턴스 생성
  5. “Save Character”는 원본 a2c를 변경하지 않고 새 .a2c를 만듦

6. a2c와 스크립트#

a2c 자체에는 Alice UI에서 직접 코드를 추가할 수 없다. 하지만 월드로 가져온 후에는 일반 World 오브젝트처럼 취급되므로 ScriptResponse 블록 등으로 Script를 작성할 수 있다.

# 월드에 가져온 bopper 조작
bopper.color = alice.red
alice.MoveAnimation(subject=bopper, direction=alice.forward, amount=1)

Alice 2 - elementData.xml 스키마#

대상: elementData.xml의 태그 구조와 Property 직렬화 규칙 출처: Element.java (internalStore / load), Property 하위 클래스들의 encode/decode 마지막 업데이트: 2026-06-09


1. 기본 구조#

flowchart LR
subgraph XML["elementData.xml"]
ROOT["<element class=#quot;...#quot; name=#quot;...#quot; version=#quot;...#quot;>"]
CHILD["<child filename=#quot;...#quot;/>"]
PROP["<property name=#quot;...#quot;>...</property>"]
end
ROOT -->|0..n| CHILD
ROOT -->|0..n| PROP
CHILD -->|ZIP 하위 디렉토리| SUB["하위 elementData.xml"]
PROP -->|Class.forName| FIELD["Element의 Property 필드"]
PROP -->|encode/decode 위임| PROPCLS["Property 하위 클래스"]
PROP -->|참조 해결| REF["ReferenceResolver"]

2. 루트 노드: #

<element class="edu.cmu.cs.stage3.alice.core.World" name="world" version="2.001">
속성설명
classElement의 FQCN (Fully Qualified Class Name)
nameElement의 name Property 값
version파일 포맷 버전 (현재 2.001 또는 2.2)

class 매핑: Element.s_classnameMap에 오래된/별칭 클래스명을 최신 클래스로 매핑.

// Element.java static initializer
s_classnameMap.put("edu.cmu.cs.stage3.alice.core.responses.SequentialResponse",
edu.cmu.cs.stage3.alice.core.responses.DoInOrder.class);
s_classnameMap.put("edu.cmu.cs.stage3.alice.core.responses.ProxyForScriptDefinedResponse",
edu.cmu.cs.stage3.alice.core.responses.ScriptDefinedResponse.class);

3. 자식 노드: #

<child filename="camera"/>
<child filename="my first method"/>
<child filename="__Unnamed0__"/>
  • filename = ZIP 내 상대 디렉토리명, getRepr(i)로 결정
  • named child: child.name.getStringValue() = 디렉토리명
  • unnamed child: "__Unnamed" + i 형식

로드 시: setCurrentDirectory(filename) > 재귀 load > addChild > 복귀


4. Property 태그 종류#

4.1 기본형 (primitive + String)#

<property class="java.lang.Double" name="opacity">1.0</property>
<property class="java.lang.Boolean" name="isFirstClass">false</property>
<property name="name">world</property>
  • class 속성 생략 시 기본값 java.lang.String
  • name은 Element의 Property 필드명과 일치

4.2 Color#

<property name="color">
<red>1.0</red><green>1.0</green><blue>1.0</blue><alpha>1.0</alpha>
</property>

4.3 Matrix4 / visualScale#

<property name="visualScale">
<row><item>0.00512</item><item>0.0</item><item>0.0</item></row>
<row><item>0.0</item><item>0.00512</item><item>0.0</item></row>
</property>

4x4 행렬 = <row> 4개, 각 <row><item> 4개.

4.4 내부 참조 (Element 간 연결)#

<property criterionClass="edu.cmu.cs.stage3.alice.core.criterion.InternalReferenceKeyedCriterion"
name="userDefinedResponse"><![CDATA[my first method]]></property>
  • criterionClass = InternalReferenceKeyedCriterion
  • 값 = target Element의 key (name)
  • 로드 시: ReferenceResolver가 key로 target 검색

예 - behavior의 triggerResponse:

<property criterionClass="..."
name="triggerResponse">behavior0.__Unnamed1__</property>

4.5 파일 참조 (ScriptProperty)#

<property name="script">java.io.File[script.py]</property>
  • ScriptProperty의 외부 파일 참조. java.io.File[파일명] 형식
  • 로드 시 파일명 추출 > ZIP에서 읽기
  • 자세한 내용: 09-storing-scripts.md

4.6 Enum#

<property class="edu.cmu.cs.stage3.alice.scenegraph.FillingStyle"
name="fillingStyle">edu.cmu.cs.stage3.alice.scenegraph.FillingStyle[SOLID]</property>

PackagePath[ENUM_NAME] 형식.

4.7 Dictionary (data Property)#

<property name="data">
<entry>
<key class="java.lang.String">edu.cmu.cs.stage3.alice.authoringtool.worldOpenTime</key>
<value class="java.lang.String">1980594</value>
</entry>
</property>

4.8 Array (responses, behaviors 등)#

<property componentClass="edu.cmu.cs.stage3.alice.core.Response" name="responses">
<item criterionClass="..."><![CDATA[my first method]]></item>
</property>

5. Element 클래스별 주요 Property#

World (Sandbox)#

Property타입설명
scriptFile 참조월드 스크립트
responsesArray실행 가능한 Response
behaviorsArray등록된 Behavior
variablesArray전역 변수
questionsArray질문(Question)
textureMapsArray텍스처 맵
dataDictionaryAuthoringTool 설정

Response (ScriptResponse, UserDefinedResponse 등)#

Property타입설명
scriptFile 참조ScriptProperty -> .py 파일
durationDouble실행 지속 시간 (초)
isCommentedOutBoolean주석 처리 여부
componentResponsesArray자식 Response들
requiredFormalParametersArray메서드 파라미터
localVariablesArray지역 변수

Behavior (WorldStartBehavior, KeyEventBehavior 등)#

Property타입설명
isEnabledBoolean활성화 여부 (기본 true)
detailsArray상세 정보 (키 코드 등)
triggerResponseInternalReference트리거할 Response
multipleRuntimeResponsePolicyEnum중복 실행 정책

Model#

Property타입설명
colorColor모델 색상
opacityDouble불투명도
fillingStyleEnum채우기 스타일
visualScaleMatrix4변환
diffuseColorMapInternalReference텍스처 참조

Variable#

Property타입설명
valueValue (any)변수 값
valueClassClass값의 Java 타입

변수 값 타입에 따라 XML 직렬화 방식이 달라짐:

  • 숫자/문자열/Boolean: 인라인 값
  • 오브젝트 참조: InternalReferenceKeyedCriterion

6. UserDefinedResponse 전체 예시#

<?xml version="1.0" encoding="UTF-8"?>
<element
class="edu.cmu.cs.stage3.alice.core.response.UserDefinedResponse"
name="my first method" version="2.001">
<property name="isFirstClass">false</property>
<property name="data"/>
<property name="isCommentedOut"/>
<property name="duration"/>
<property name="componentResponses"/>
<property name="requiredFormalParameters"/>
<property name="keywordFormalParameters"/>
<property name="localVariables"/>
</element>

비어 있는 Property (null)는 빈 태그로 저장. Alice는 null을 “기본값 사용”으로 해석.


7. Property Encoding 위임 구조#

Property 클래스XML 저장 방식
StringProperty텍스트 노드
NumberPropertyclass 속성 + 텍스트 노드
BooleanPropertytrue / false
ColorProperty<red>/<green>/<blue>/<alpha>
Matrix44Property<row>/<item>
DictionaryProperty<entry>/<key>/<value>
ElementArrayPropertycomponentClass + <item>
ClassPropertyFQCN 문자열
EnumPropertyPackagePath[ENUM_NAME]
ScriptPropertyjava.io.File[filename.py]
ValueProperty값 타입에 따라 적절한 방식 선택

Alice 2 - 스크립트가 파일에 저장/로드되는 방식#

대상: ScriptProperty 직렬화 메커니즘과 4가지 스크립트 입력면의 저장 차이 출처: ScriptProperty.java, ScriptResponse.java, ScriptDefinedResponse.java 분석 기준: alice2 source + Alice 2.6.1 배포본.


1. ScriptProperty 개요#

ScriptPropertyStringProperty의 하위 클래스로, 문자열 스크립트를 별도의 .py 파일로 저장한다.

classDiagram
class Property {
+encode(Document, Node, Storer, ReferenceGenerator)
+decode(Node, Loader, Vector, double)
}
class StringProperty {
+String getStringValue()
}
class ScriptProperty {
-Code m_code
-Object m_associatedFileKey
+getCode(CompileType) Code
+encodeObject()
+decodeObject()
+loadScript(InputStream) String
+storeScript(OutputStream)
+keepAnyAssociatedFiles()
}
Property <|-- StringProperty
StringProperty <|-- ScriptProperty

핵심 특징#

기능설명
m_code 캐시Jython 컴파일 결과 캐시, set()null 초기화
m_associatedFileKeyZIP 내 .py 파일의 keep key (변경 감지용)
getCode(compileType)지연 컴파일 (lazy compilation)
파일명getName() + ".py" (Property명 + “.py”)

2. 저장 (encodeObject) 상세#

sequenceDiagram
participant E as Element.internalStore()
participant SP as ScriptProperty
participant ST as DirectoryTreeStorer
participant ZIP as "[a2w] ZIP Archive"
E->>SP: Property 저장 요청
SP->>SP: filename = "script.py"
SP->>ST: storer.getKeepKey("script.py")
ST-->>SP: associatedFileKey (or null)
alt 변경 또는 신규
SP->>ST: createFile("script.py"), storeScript(os), close
else 변경 없음
SP->>ST: keepFile("script.py")
end
SP->>SP: node.appendChild("java.io.File[script.py]")
E->>ST: writeXMLDocument → elementData.xml
protected void encodeObject(...) {
String filename = getName()+".py";
Object associatedFileKey = storer.getKeepKey(filename);
if (m_associatedFileKey==null || !m_associatedFileKey.equals(associatedFileKey)) {
m_associatedFileKey = null;
OutputStream os = storer.createFile(filename, true);
storeScript(os);
storer.closeCurrentFile();
m_associatedFileKey = storer.getKeepKey(filename);
} else {
storer.keepFile(filename);
}
node.appendChild(createNodeForString(document, "java.io.File["+filename+"]"));
}

3. 로드 (decodeObject) 상세#

sequenceDiagram
participant E as Element.load()
participant SP as ScriptProperty
participant LD as DirectoryTreeLoader
participant ZIP as "[a2w] ZIP Archive"
E->>E: XML > property name="script"
E->>SP: decodeObject(node, loader, refs, version)
SP->>SP: getNodeText(node) = "java.io.File[script.py]"
SP->>SP: getFilename() = "script.py"
SP->>LD: readFile("script.py")
LD-->>SP: InputStream
SP->>SP: loadScript(is) - BufferedReader로 한 줄씩 읽음
SP->>SP: set(scriptString) - m_code = null
SP->>LD: getKeepKey("script.py") - keep key 저장
private String loadScript(InputStream is) throws IOException {
BufferedReader br = new BufferedReader(
new InputStreamReader(new BufferedInputStream(is)));
StringBuffer sb = new StringBuffer();
while (true) {
String s = br.readLine();
if (s != null) {
sb.append(s);
sb.append('\n');
} else break;
}
if (sb.length() > 0)
return sb.substring(0, sb.length() - 1);
return "";
}

4. 4가지 스크립트 입력면 저장 위치 비교#

입력면PropertyZIP 내 파일 경로
Sandbox.script (World)ScriptProperty/script.py
ScriptResponse.scriptScriptProperty/behaviorN/__UnnamedN__/script.py
ScriptDefinedResponse.scriptScriptProperty/behaviorN/__UnnamedN__/script.py
ComboWidget / ScratchPad없음 (메모리)저장되지 않음

중요: ScriptResponse와 ScriptDefinedResponse도 ScriptProperty를 사용한다! 세 가지 모두 ZIP 내 별도의 .py 파일로 저장된다. 유일한 차이는 ZIP 내 경로.

flowchart LR
subgraph ZIP["world.a2w ZIP"]
SPY["/script.py (Sandbox.script)"]
RP1["/behavior0/__Unnamed0__/script.py (ScriptResponse)"]
RP2["/behavior0/__Unnamed1__/script.py (ScriptDefinedResponse)"]
EX["/elementData.xml"]
end
EX -->|"<property name=#quot;script#quot;>java.io.File[script.py]</property>"| SPY
EX -->|behavior0 child| B0["behavior0/elementData.xml"]
B0 -->|__Unnamed0__ child| U0["__Unnamed0__/elementData.xml"]
U0 -->|script property →| RP1
B0 -->|__Unnamed1__ child| U1["__Unnamed1__/elementData.xml"]
U1 -->|script property →| RP2

5. 코드 컴파일과 캐싱#

public Code getCode(CompileType compileType) {
String script = getStringValue();
if (script != null && script.length() > 0) {
if (m_code == null) {
m_code = getOwner().compile(script, this, compileType);
}
} else {
m_code = null;
}
return m_code;
}
  1. script 빈 문자열 → m_code = null 반환
  2. m_code == nullowner.compile()으로 Jython 컴파일
  3. 결과 캐시, set() 호출 시 m_code = null로 초기화

6. 유저 입장 저장 요약#

편집 위치저장 위치지속성
Edit Script > ScriptEditor/script.py in a2w✅ 저장됨
ScriptResponse 블록/.../script.py in a2w✅ 저장됨
ScriptDefinedResponse 블록/.../script.py in a2w✅ 저장됨
ScriptComboWidget (Go)메모리 전용❌ 휘발
ScriptScratchPad (Perform All)메모리 전용❌ 휘발

ComboWidget/ScratchPad 코드는 저장되지 않는다! 영구 유지하려면 ScriptResponse 블록이나 Sandbox Script로 옮겨야 함.


7. 변수/이벤트와 스크립트 관계#

변수 (Variable)#

Alice UI에서 만든 전역 변수는 Sandbox.variables 배열에 저장:

<property componentClass="edu.cmu.cs.stage3.alice.core.Variable" name="variables">
<item criterionClass="...">myVariable</item>
</property>

각 Variable Element는 별도 디렉토리에 저장:

/myVariable/elementData.xml
<element class="edu.cmu.cs.stage3.alice.core.Variable" name="myVariable">
<property class="java.lang.Double" name="value">42.0</property>
<property class="java.lang.Class" name="valueClass">java.lang.Number</property>
</element>

스크립트에서 접근:

# Alice UI 변수는 이름으로 직접 접근 (Sandbox.lookup 자동)
print(myVariable)
# Python 지역 변수 (Alice 트리와 무관)
myNewVar = 100

Behavior와 Response 연결#

triggerResponse Property로 Behavior가 실행할 Response 지정:

<element class="edu.cmu.cs.stage3.alice.core.behavior.WorldStartBehavior" name="behavior0">
<property criterionClass="..." name="triggerResponse">behavior0.__Unnamed1__</property>
</element>

이벤트 등록 (KeyEventBehavior)#

<element class="edu.cmu.cs.stage3.alice.core.behavior.KeyEventBehavior" name="keyHandler">
<property name="keyCode">32</property> <!-- VK_SPACE -->
<property name="keyEventType">KEY_PRESSED</property>
<property criterionClass="..." name="triggerResponse">myScriptResponse</property>
</element>

8. ⚠️ SoundAction → SoundResponse 호환성 문제 (a2w 로딩)#

현상#

Alice 2.2 등 이전 버전에서 만든 a2w 파일에 SoundAction 클래스명이 저장되어 있으면 Alice 2.6.1에서 로딩 실패.

원인#

Element.s_classnameMapSoundAction → SoundResponse 매핑이 없음.

// Element.java static initializer — 확인된 매핑 목록
// SoundAction → SoundResponse 매핑 없음!

a2w ZIP 내부에서 확인하는 방법#

  1. a2w를 ZIP으로 열기
  2. 모든 elementData.xml에서 다음 문자열 검색:
    class="edu.cmu.cs.stage3.alice.core.responses.SoundAction"
  3. 발견 시 SoundActionSoundResponse로 수동 변경

수동 수정#

<!-- before (로드 실패) -->
<element class="edu.cmu.cs.stage3.alice.core.responses.SoundAction" ...>
<!-- after (로드 성공) -->
<element class="edu.cmu.cs.stage3.alice.core.responses.SoundResponse" ...>

이 수정으로 ZIP을 저장하면 Alice 2.6.1에서 정상 로드된다.

Alice 2 - 드래그 앤 드롭 블록 생성 메커니즘#

대상: ResponseEditor 팔레트에서 블록을 드래그하여 Element 인스턴스가 생성되는 전체 과정 출처: ResponseEditor.java, DnDGroupingPanel.java, CompositeComponentElementPanel.java, ElementPrototype.java, CompositeComponentResponsePanel.java, GUIFactory.java 마지막 업데이트: 2026-06-09


1. 전체 구조 개요#

flowchart TD
subgraph Palette["팔레트 (initPrototypes)"]
DIO["doInOrderPrototype: DnDGroupingPanel"]
DT["doTogetherPrototype: DnDGroupingPanel"]
SCRIPT["scriptPrototype: DnDGroupingPanel<br/>label='script'"]
SDR["scriptDefinedPrototype: DnDGroupingPanel<br/>label='script defined'"]
IF["doIfTruePrototype: DnDGroupingPanel"]
LOOP["loopPrototype: DnDGroupingPanel"]
WL["whileLoopPrototype: DnDGroupingPanel"]
end
Palette -->|"DragGestureListener.dragGesturePerformed()"| DND["java.awt.dnd.DragSource"]
DND -->|"startDrag() → Transferable"| TRANS["ResponsePrototypeReferenceTransferable<br/>ResponsePrototype 래핑"]
TRANS -->|"DropTarget.drop()"| DROP["CompositeComponentResponsePanel.drop()"]
DROP -->|"getTransferData(responsePrototypeReferenceFlavor)"| RP["ResponsePrototype"]
RP -->|"createNewResponse() → createNewElement()"| NEW["새 Response 인스턴스<br/>(ScriptResponse, DoInOrder 등)"]
NEW -->|"performDrop(response, dtde)"| TREE["Element 트리에 addChild"]
style SCRIPT fill:#ff0,stroke:#a80
style SDR fill:#ff0,stroke:#a80
style NEW fill:#aef,stroke:#08a
style RP fill:#fda,stroke:#a60

2. 팔레트 초기화 (initPrototypes)#

ResponseEditor가 열릴 때, initPrototypes()가 각 블록 타입별로 DnDGroupingPanel + ResponsePrototype을 생성한다.

// ResponseEditor.initPrototypes()
protected void initPrototypes() {
// 1. 각 블록 타입의 표시 문자열 가져오기
String scriptString = AuthoringToolResources.getReprForValue(
ScriptResponse.class); // → "script"
// 2. DnDGroupingPanel 생성 (Swing JPanel, 드래그 가능)
scriptPrototype = new DnDGroupingPanel();
scriptPrototype.setBackground(SCRIPT_COLOR); // 노란색 계열
// 3. 라벨 추가 (팔레트에 표시될 텍스트)
JLabel scriptLabel = new JLabel(scriptString);
scriptPrototype.add(scriptLabel, BorderLayout.CENTER);
// 4. ★핵심★ Transferable 설정 (드래그 시 전달할 데이터)
scriptPrototype.setTransferable(
new ResponsePrototypeReferenceTransferable(
new ResponsePrototype(ScriptResponse.class, null, null)));
// 5. 드래그 소스 등록
scriptPrototype.addDragSourceComponent(scriptLabel);
// ScriptDefinedResponse도 동일한 패턴
scriptDefinedPrototype = new DnDGroupingPanel();
// ... 같은 패턴 ...
scriptDefinedPrototype.setTransferable(
new ResponsePrototypeReferenceTransferable(
new ResponsePrototype(ScriptDefinedResponse.class, null, null)));
}

각 블록별로 생성되는 Prototype:

블록Response 클래스knownPropertyValuesdesiredProperties
DoInOrderDoInOrder.classnullnull
DoTogetherDoTogether.classnullnull
If/ElseIfElseInOrder.classnull{"condition"}
Loop N timesLoopNInOrder.classnull{"end"}
WhileWhileLoopInOrder.classnull{"condition"}
ForEachForEachInOrder.classnull{"list"}
scriptScriptResponse.classnullnull
script definedScriptDefinedResponse.classnullnull
CommentComment.class{"text", "No comment"}null
PrintPrint.classnullnull

knownPropertyValues: 생성 시 미리 설정할 Property 값 desiredProperties: 드롭 후 팝업 메뉴에서 사용자가 입력해야 할 Property 목록


3. 드래그 시작 (Drag Source)#

sequenceDiagram
participant User as 유저
participant DGP as DnDGroupingPanel
participant GDL as GroupingPanelDragGestureListener
participant DS as DragSource
participant TRANS as ResponsePrototypeReferenceTransferable
participant DNDM as DnDManager
User->>DGP: 팔레트의 "script" 블록 그립(grip) 클릭 + 드래그
DGP->>GDL: dragGestureRecognizer 감지
GDL->>GDL: dragGesturePerformed(DragGestureEvent dge)
alt Transferable이 null이 아님
GDL->>DS: dge.startDrag(DefaultCopyDrop, transferable, listener)
DS->>DNDM: DnDManager.fireDragStarted(transferable, this)
else Transferable이 null
GDL->>DS: startDrag(DefaultCopyNoDrop, emptyImg, ...)
Note over GDL: 드래그 불가, NoDrop 커서 표시
end
Note right of DS: 이제 Transferable이<br/>Drag-and-Drop 시스템을 통해<br/>DropTarget으로 전달됨

DnDGroupingPanel의 핵심 드래그 코드:

DnDGroupingPanel.GroupingPanelDragGestureListener
public void dragGesturePerformed(DragGestureEvent dge) {
if (DnDGroupingPanel.this.transferable != null) {
dge.startDrag(DragSource.DefaultCopyDrop,
DnDGroupingPanel.this.transferable, // ← Transferable 객체
DnDManager.getInternalListener());
DnDManager.fireDragStarted(DnDGroupingPanel.this.transferable, this);
} else {
// 드래그 불가 — 빈 이미지로 시작 (NoDrop 커서)
dge.startDrag(DragSource.DefaultCopyNoDrop, empty, new Point(), ...);
}
}

4. 데이터 전달 (Transferable)#

ResponsePrototypeReferenceTransferable이 드래그 데이터를 캡슐화한다.

public class ResponsePrototypeReferenceTransferable
extends ElementPrototypeReferenceTransferable {
// 이 Transferable이 지원하는 DataFlavor
public final static DataFlavor responsePrototypeReferenceFlavor =
new DataFlavor(
DataFlavor.javaJVMLocalObjectMimeType
+ "; class=edu.cmu.cs.stage3.alice.authoringtool.util.ResponsePrototype",
"responsePrototypeReferenceFlavor");
protected ResponsePrototype responsePrototype;
public ResponsePrototypeReferenceTransferable(
ResponsePrototype responsePrototype) {
super(responsePrototype);
this.responsePrototype = responsePrototype;
// 3가지 flavor 지원
flavors = new DataFlavor[3];
flavors[0] = responsePrototypeReferenceFlavor; // Response 전용
flavors[1] = elementPrototypeReferenceFlavor; // 범용 Element
flavors[2] = DataFlavor.stringFlavor; // 디버그용 문자열
}
public Object getTransferData(DataFlavor flavor) {
if (flavor.equals(responsePrototypeReferenceFlavor))
return responsePrototype; // ← ResponsePrototype 객체 반환
if (flavor.equals(elementPrototypeReferenceFlavor))
return responsePrototype;
if (flavor.equals(DataFlavor.stringFlavor))
return responsePrototype.toString();
throw new UnsupportedFlavorException(flavor);
}
}

지원되는 DataFlavor 3종:

Flavor용도반환 객체
responsePrototypeReferenceFlavorResponseEditor 전용 드롭ResponsePrototype
elementPrototypeReferenceFlavor범용 Element 드롭 처리ResponsePrototype
stringFlavor디버깅/폴백responsePrototype.toString()

5. 드롭 처리 (Drop Target)#

sequenceDiagram
participant DROP as CompositeComponentResponsePanel<br/>(DropTarget)
participant DNDM as DnDManager
participant TRANS as Transferable
participant RP as ResponsePrototype
participant EP as ElementPrototype
DROP->>DROP: drop(DropTargetDropEvent dtde)
DROP->>DNDM: getCurrentTransferable()
DROP->>TRANS: safeIsDataFlavorSupported(responsePrototypeReferenceFlavor)?
alt Prototype 드롭 (팔레트에서)
DROP->>TRANS: getTransferData(responsePrototypeReferenceFlavor)
TRANS-->>DROP: ResponsePrototype
DROP->>RP: getDesiredProperties().length
alt desiredProperties 없거나 3개 초과
RP->>EP: createNewElement() → Class.newInstance()
EP-->>DROP: 새 ScriptResponse 인스턴스
DROP->>DROP: performDrop(newResponse, dtde)
else desiredProperties 1~3개
DROP->>DROP: 팝업 메뉴 표시 (사용자 입력)
User->>DROP: 팝업에서 값 선택
DROP->>RP: createNewElement() (knownPropertyValues 적용)
DROP->>DROP: performDrop(newResponse, dtde)
end
else Property 드롭 (속성 패널에서)
DROP->>TRANS: getTransferData(propertyReferenceFlavor)
TRANS-->>DROP: Property 객체
DROP->>DROP: PropertyAnimation Response 생성
DROP->>DROP: performDrop(animation, dtde)
else 기존 Response 이동/복사
DROP->>TRANS: getTransferData(responseReferenceFlavor)
TRANS-->>DROP: Response 객체
alt ACTION_MOVE
DROP->>DROP: performDrop(response, dtde) → shift
else ACTION_COPY
DROP->>DROP: performDrop(response.copy(), dtde) → add
end
end

6. ElementPrototype.createNewElement() — ★핵심 공장 메서드★#

ElementPrototype.createNewElement()가 실제로 Java 리플렉션을 통해 Element 인스턴스를 생성한다.

flowchart TD
START["createNewElement()"] --> NEW["elementClass.newInstance()<br/>예: new ScriptResponse()"]
NEW --> KNOWN{"knownPropertyValues<br/>있음?"}
KNOWN -->|각 knownPropertyValue에 대해| PROP["property.set(value)"]
PROP -->|value가 Element면| ADD["property.owner.addChild(valueElement)"]
KNOWN -->|끝| SPECIAL{"특수 클래스인가?"}
SPECIAL -->|ForEach| FOREACH["item Variable 생성 + addChild"]
SPECIAL -->|LoopNInOrder| LOOP["index Variable 생성 + addChild"]
SPECIAL -->|기타| DONE["Element 반환"]
FOREACH --> DONE
LOOP --> DONE
// ElementPrototype.createNewElement() — 핵심 로직
public Element createNewElement() {
// 1. Class.newInstance()로 Element 생성
Element element = (Element)elementClass.newInstance();
// 2. knownPropertyValues 설정 (미리 지정된 값)
if (knownPropertyValues != null) {
for (int i = 0; i < knownPropertyValues.length; i++) {
String propertyName = knownPropertyValues[i].getString();
Object propertyValue = knownPropertyValues[i].getObject();
Property property = element.getPropertyNamed(propertyName);
property.set(propertyValue);
// value가 Element이면 owner의 child로 추가
if (propertyValue instanceof Element) {
Element e = (Element)propertyValue;
if (e.getParent() == null && !(e instanceof World)) {
property.getOwner().addChild(e);
e.data.put("associatedProperty", property.getName());
}
}
}
}
// 3. 특수 클래스 처리 — 자동 변수 생성
if (ForEachInOrder.class.isAssignableFrom(elementClass)) {
Variable eachVar = new Variable();
eachVar.name.set("item");
eachVar.valueClass.set(Object.class);
element.addChild(eachVar);
((ForEachInOrder)element).each.set(eachVar);
} else if (LoopNInOrder.class.isAssignableFrom(elementClass)) {
Variable indexVar = new Variable();
indexVar.name.set("index");
indexVar.valueClass.set(Number.class);
element.addChild(indexVar);
((LoopNInOrder)element).index.set(indexVar);
}
// ... WhileLoop 등 유사 패턴
return element;
}

7. performDrop() — Element 트리에 연결#

드롭된 Element가 실제 Alice Element 트리에 추가되는 마지막 단계.

flowchart TD
DROP["performDrop(toDrop, dtde)"] --> RECURSE{"재귀 호출?"}
RECURSE -->|예| WARN["경고 다이얼로그 표시<br/>취소 가능"]
RECURSE -->|아니오| COPY{"ACTION_COPY?"}
COPY -->|예| CLONE["toDrop = toDrop.HACK_createCopy()"]
COPY -->|아니오| MOVE{"이미 같은 배열에<br/>있는 Element?"}
MOVE -->|예| SHIFT["componentElements.shift(old, new)"]
MOVE -->|아니오| ADD["unhook(toDrop) → removeFromParent()"]
ADD -->|"addToElement(toDrop, componentElements, position)"| FINAL
SHIFT --> FINAL
CLONE --> ADD2["addToElement(toDrop, componentElements, position)"]
ADD2 --> FINAL
FINAL["✅ UndoRedoStack.stopCompound()"]
// CompositeComponentElementPanel.performDrop() — 최종 Element 트리 삽입
protected void performDrop(Element toDrop, DropTargetDropEvent dtde) {
// 재귀 호출 검사
if (isRecursive(toDrop)) {
// 경고 다이얼로그 → 사용자가 취소 가능
int result = DialogManager.showOptionDialog(...);
if (result != 0) return;
}
if (authoringTool != null)
authoringTool.getUndoRedoStack().startCompound();
int position = getInsertLocation(...);
if ((dtde.getDropAction() & ACTION_COPY) > 0) {
// ★복사: createCopy로 Element 복제
toDrop = toDrop.HACK_createCopy(null, null, position, null, ...);
}
if (!isCopy && componentElements.contains(toDrop)) {
// ★이동: 같은 배열 내에서 위치 변경
int oldIndex = componentElements.indexOf(toDrop);
position = adjustPosition(oldIndex, position);
componentElements.shift(oldIndex, position);
} else {
// ★신규: 부모 변경 후 ObjectArrayProperty에 add
unhook(toDrop); // 기존 부모에서 제거
addToElement(toDrop, componentElements, position);
}
if (authoringTool != null)
authoringTool.getUndoRedoStack().stopCompound();
}

8. 블록별 드롭 결과 예시#

”script” 블록 드롭#

팔레트 [script] → 드래그 → DoInOrder 위에 드롭
ResponsePrototype(ScriptResponse.class, null, null)
ElementPrototype.createNewElement()
new ScriptResponse() ← ScriptProperty(script="", duration=0)
componentElements.add(position, scriptResponse)
DoInOrder의 자식 Response로 등록

”script defined” 블록 드롭#

팔레트 [script defined] → 드래그 → DoInOrder 위에 드롭
ResponsePrototype(ScriptDefinedResponse.class, null, null)
new ScriptDefinedResponse() ← ScriptProperty(script="")
componentElements.add(position, scriptDefinedResponse)

속성에서 PropertyAnimation 드롭#

bunny.color 속성을 드래그 → DoInOrder 위에 드롭
PropertyAnimation.response 팝업 (value/지속시간 선택)
new PropertyAnimation()
element = property.getOwner() // bunny
propertyName = "color"
value = 사용자 선택 값

9. 키 포인트 요약#

  1. 팔레트의 각 블록은 DnDGroupingPanel + ResponsePrototype이다.
  2. 드래그 시 ResponsePrototypeReferenceTransferableResponsePrototype 객체를 전달한다.
  3. 드롭 시 CompositeComponentResponsePanel.drop()responsePrototypeReferenceFlavor 로 Prototype을 추출한다.
  4. ElementPrototype.createNewElement()Class.newInstance() 로 Element를 생성하고, knownPropertyValues를 설정한다.
  5. performDrop()Element 트리의 ObjectArrayProperty 에 새 Element를 추가한다.
  6. 복사/이동/신규 생성 모두 동일한 performDrop()을 통해 처리된다.
  7. ForEach/LoopN 등 특수 블록은 자동으로 index/item 변수를 생성한다.
  8. Undo/Redo는 startCompound()/stopCompound()로 그룹핑된다.

ScriptResponse와 ScriptDefinedResponse의 차이는 createNewElement() 시점에는 없다.
둘 다 동일한 패턴으로 new ScriptResponse() / new ScriptDefinedResponse() 인스턴스만 생성된다.
차이는 런타임에 CompileType.EXEC_MULTIPLE vs CompileType.EVAL로 나타난다.


11 - 문제 해결: 실전 문제 해결#

Alice 2.6.1에서 a2w 월드를 만들고 수정할 때 실제로 마주친 문제들과 해결 방법. 분석 기준: 분할주의력검사 월드 개발 과정에서 확인된 내용.


1. 월드 로딩 실패 — “Unable to load world”#

케이스 A: SoundAction ClassNotFoundException#

에러 메시지:

ClassNotFoundException: edu.cmu.cs.stage3.alice.core.response.SoundAction

원인: Alice 2의 블록 편집기에서 SoundAction 클래스명으로 저장된 블록이 있는데, Element.s_classnameMapSoundAction → SoundResponse 매핑이 없다.

해결: a2w ZIP 파일을 직접 열어 elementData.xml에서 SoundAction을 검색하여 SoundResponse로 변경.

1. a2w 파일을 ZIP 유틸리티(7-Zip 등)로 열기
2. 모든 elementData.xml에서 "SoundAction" 검색
3. class="..." 속성을 SoundResponse로 변경
- "edu.cmu.cs.stage3.alice.core.responses.SoundAction"
→ "edu.cmu.cs.stage3.alice.core.responses.SoundResponse"
4. ZIP 저장 후 다시 로드

케이스 B: Property.decodeObject RuntimeException#

에러 메시지:

java.lang.RuntimeException
at edu.cmu.cs.stage3.alice.core.Property.decodeObject(Property.java:663)

원인: elementData.xml의 Property 값 디코딩 실패. 특정 Property가 예상 타입과 다른 값이 저장된 경우. 주로 변수 valueClass 불일치, Reference 해석 실패 등.

해결:

  • Alice에서 “Yes” 눌러 None으로 설정하고 계속 진행
  • 이후 해당 Property를 UI에서 직접 재설정
  • 또는 ZIP 내 elementData.xml을 열어 해당 Property 값 확인/수정

케이스 C: InternalReferenceKeyedCriterion 경고#

경고 메시지:

WARNING: unable to resolve references:
...triggerResponse → ...InternalReferenceKeyedCriterion[event2.Unnamed0]

원인: ZIP 내에 참조 대상 디렉토리(__Unnamed0__)가 없거나, 참조 키가 불일치. 블록 편집기에서 behavior의 triggerResponse 연결이 깨짐.

해결:

  1. “Yes” 눌러 로딩 계속
  2. Alice UI에서 해당 behavior의 triggerResponse 속성 재할당
  3. 또는 ZIP 내 elementData.xml에서 criterionClass 참조 수정

2. 수정된 월드가 로딩 시 Reference Warning#

현상: 팀원이 저장한 a2w를 열면 수많은 unable to resolve references 경고가 뜸

원인: ZIP 내 unnamed child 디렉토리명(__UnnamedN__)의 인덱스가 elementData.xml의 참조와 불일치. 블록 편집기에서 복사/이동/삭제 시 내부 참조가 깨질 수 있음.

해결:

  1. “Yes” 누르고 로딩 (모든 참조 None 처리)
  2. Alice UI에서 behavior들의 triggerResponse를 다시 연결
  3. 저장 전에 모든 behavior 연결 확인

3. stopSign이 car와 함께 움직임#

현상: carMoveLoop에서 car가 앞으로 갈 때 stopSign도 따라 움직임

원인: stopSign의 vehicle Property가 car로 설정되어 있거나, carMoveLoop에서 stopSign move backward 0.5 동기화 코드가 들어가 있음. 블록 구성에 따라 의도된 동작일 수도 있음 (stopSign을 car-relative로 배치한 경우).

확인 방법: Alice UI에서 stopSign 선택 → vehicle 속성 확인

  • vehicle = world: stopSign은 고정 (car와 무관)
  • vehicle = car: stopSign은 car의 자식처럼 움직임

수정: stopSign의 vehicle을 world로 설정하거나, carMoveLoop에서 stopSign 이동 블록 제거


4. SayAnimation에서 변수 값이 안 보임#

현상:

car.say("결과: ${score}점") # 스크립트는 동작하지 않음

또는 블록으로 car say score 했는데 레이블 없이 숫자만 뜸

원인 & 해결:

  1. Script는 동작하지 않음 — Alice 2.6.1 Jython NPE 문제 (01-architecture.md 참고)
  2. 한글이 안 보임 — Alice 2의 Text3D/SayAnimation은 CJK 글리프를 지원하지 않음
  3. 레이블 + 값 표시DoTogether로 여러 SayAnimation 블록 연결:
DoTogether:
car → say "Score: " (0.1초)
car → say score (0.1초)
car → say " | Wrong: " (0.1초)
car → say wrongCount (0.1초)
  1. Text3D 오브젝트 사용 — 미리 ScoreText 등의 Text3D를 배치해두고 text Property를 블록의 PropertyAnimation으로 업데이트 → 하지만 3D 공간에 위치하므로 차가 이동하면 화면 밖으로 나감

5. Print 구문이 안 보임#

현상: ScriptScratchPad에서 print("test") 실행했는데 아무 출력도 안 보임

원인:

  • Alice 2의 System.out은 기본적으로 Alice 로그 창으로 리디렉션
  • ScriptingFactory.setStdOut()를 호출해야 출력 스트림이 연결됨
  • UI 상에 출력창이 보이지 않을 수 있음

대안:

  • Print 블록 사용 (ResponseEditor 팔레트에 있음)
  • SayAnimation 블록으로 car 등에 말풍선 표시

6. 테스트 종료 후에도 car가 계속 움직임#

현상: checkEndCondition에서 testRunning = 0으로 설정했는데도 carMoveLoop가 계속 돎

원인: WhileLoopInOrder는 조건을 behavior 실행 시점에 평가. testRunning = 0이 설정된 후 다음 behavior 스케줄 시점까지 딜레이가 있을 수 있음. 또는 WhileLoopInOrder의 condition이 isShowing 등 다른 Property를 보고 있을 가능성.

해결 방법:

  • WhileLoop의 condition이 testRunning == 1 (Variable 비교)인지 확인
  • checkEndCondition 메서드가 WhileLoop보다 먼저 실행되는지 behavior 순서 확인
  • 또는 WhileLoop 내부에 If testRunning == 0 → break 블록 추가

7. 한글/글자 깨짐#

원인: Alice 2.6.1의 3D 텍스트 렌더러(Text3D, SayAnimation)가 CJK(CJK Unified Ideographs) 글리프를 지원하지 않음. 기본 폰트에 한글 글리프가 없음.

해결:

  • 모든 사용자 텍스트를 영어로 작성
  • 정답: 5Correct: 5 / Score: 5
  • 오답: 2Wrong: 2
  • 평균 반응시간Avg Time
  • 검사 결과Results
  • 시작하려면 스페이스바Press SPACE to start

8. 변수 값이 SayAnimation에 반영 안 됨#

현상: score 값을 SayAnimation으로 표시했는데 항상 0

원인:

  • SayAnimation(what=score)variable reference를 사용
  • variable이 업데이트되어도 SayAnimation의 텍스트는 prologue 시점에 고정
  • 또는 SayAnimation의 duration이 0이면 화면에 표시될 시간이 없음

해결:

  • duration을 충분히 설정 (0.5~1초)
  • 값 업데이트 후 새로운 SayAnimation 실행
  • DoTogether로 여러 SayAnimation 동시 실행

9. a2w ZIP 직접 수정 가이드#

ZIP 내부를 직접 수정해야 할 때:

world.a2w/
├── elementData.xml # 월드 전체 구조
├── script.py # Sandbox Script (실행 안 됨)
├── camera/
├── car/
│ └── elementData.xml
├── stopSign/
│ └── elementData.xml
├── score/
│ └── elementData.xml
├── my first method/
│ └── elementData.xml # 메서드 정의
├── behavior0/ # World start behavior
│ ├── elementData.xml
│ └── __Unnamed0__/
│ └── elementData.xml # triggerResponse의 내용
└── event/ # 키 이벤트 behavior
├── elementData.xml
└── __Unnamed0__/
└── elementData.xml

수정 팁:

  1. ZIP 파일 바로 열기 → 7-Zip 등으로 열고 텍스트 파일 직접 편집
  2. elementData.xml에서 <property name="class"> 값 수정으로 클래스명 변경 가능
  3. 변수 값: <property class="java.lang.Double" name="value">5.0</property>
  4. 참조 복구: <property criterionClass="..." name="triggerResponse">behavior0.__Unnamed1__</property>
  5. 저장 후 Alice로 로드 확인

10. “enableScripting” 메뉴 안 보임#

현상: 우클릭 메뉴에 “Edit Script”가 없음

원인: Alice 설정에서 enableScripting = false

해결: Alice 설치 디렉토리에서 설정 파일 확인

  • Required/ 아래 설정에서 enableScripting=true로 변경
  • 또는 Alice.ini 등 설정 파일 수정