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.java의 Py.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 직접 수정)문서 목록
| # | 파일 | 내용 | 분량 |
|---|---|---|---|
| 01 | 01-architecture.md | Jython 부트스트랩, World.start(), ScriptResponse/ScriptDefinedResponse | 중 |
| 02 | 02-name-resolution.md | Namespace → PyElement 전체 체인, PySandbox 변수 할당 | 중 |
| 03 | 03-script-surfaces.md | 유저 중심: 4가지 입력면 UI + 실행 시점 | ★ 많음 |
| 04 | 04-api-reference.md | alice.* API 전수, 발견된 버그 3건 | 중 |
| 05 | 05-behind-the-scenes.md | Java 클래스 계층, s_classnameMap, 호출 체인 | 중 |
| 06 | 06-file-format-a2w.md | a2w ZIP 내부, 저장/로드 플로우 | 중 |
| 07 | 07-file-format-a2c.md | a2c 갤러리 오브젝트 포맷 | 소 |
| 08 | 08-element-data-xml.md | elementData.xml 스키마 전수 | 중 |
| 09 | 09-storing-scripts.md | ScriptProperty, .py 저장, 4가지 입력면 저장 비교 | 중 |
| 10 | 10-drag-and-drop.md | 팔레트 → ResponsePrototype → Element 인스턴스화 | 많음 |
| 11 | 11-troubleshooting.md | 실전 문제: 로딩 실패, 폰트, 변수, a2w 직접 수정 | 중 |
빠른 참조
# Script의 API 참조 — 실제로는 동작하지 않지만 구조는 이렇다
import alice
# 이름으로 접근 가능한 객체bunny.color = alice.redbunny.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 NPE | Interpreter.java 56번째 줄 null | Script 전면 불가 |
| HideAction = ShowAction | animations.py 702-703, 둘 다 isShowing=true | 버그 |
| IfElseInOrder elseAnimation 무시 | animations.py 162-173, #todo로 남음 | 미구현 |
| LoopNInOrder = None | animations.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__.py→animations.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 구현
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.javapublic 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종
| CompileType | Jython CompileMode | 특징 | 사용처 |
|---|---|---|---|
EVAL | CompileMode.eval | 단일 표현식 평가, 결과 반환 | ScriptDefinedResponse |
EXEC_SINGLE | CompileMode.single | 단일 구문 실행 | ScriptComboWidget (Go 버튼) |
EXEC_MULTIPLE | CompileMode.exec | 여러 구문 실행 | ScriptResponse, Sandbox Script, ScratchPad |
4. World.start() — 실제 코드로 보는 실행 흐름
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 실행 end5. 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 (실제 코드)
public class RuntimeScriptResponse extends RuntimeResponse { public void update(double t) { super.update(t); ScriptResponse.this.exec( ScriptResponse.this.script.getCode(CompileType.EXEC_MULTIPLE)); }}ScriptDefinedResponse (실제 코드)
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);}| ScriptResponse | ScriptDefinedResponse | |
|---|---|---|
| CompileType | EXEC_MULTIPLE | EVAL |
| 실행 시기 | 매 프레임 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__.py와animations.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 (실제 코드)
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 (실제 코드)
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의 지역변수/파라미터가 전역 오브젝트명보다 항상 우선한다.
stackLookup → detailLookup 순서로 검색.
4. PyElement.findattr_ex — 속성 읽기 (실제 코드)
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 — 변수에 값 쓰기 (실제 코드)
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 쓰기 (실제 코드)
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 prefix | child 조회 건너뛰고 바로 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.py9. ⭐ 키 포인트 요약
- 이름은 대소문자를 구분하지 않는다 (
getChildNamedIgnoreCase,equalsIgnoreCase) - Behavior의 지역변수 > 전역 오브젝트명 (stackLookup 우선)
- Alice UI 변수 = Python 할당 가능 (PySandbox.__setattr__가 가로챔)
_nameprefix = child 우회 (같은 이름의 child와 property가 있을 때 유용)__UnnamedN__= 이름 없는 child 접근 (DefinedResponse 내부 구조 분석용)/경로 = 계층적 접근 (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.java56번째 줄 —Py.compile_flags(script, source, mode, null)마지막 인자가 null ScriptingFactory.java에서 builtins 로딩할 때는Py.getCompilerFlags()를 제대로 사용
| 입력면 | 시점 | 결과 |
|---|---|---|
| Sandbox Script | World.start() 시 | NPE |
| ScriptResponse | 매 프레임 update() | NPE |
| ScriptDefinedResponse | prologue() 시 | 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 — EditScriptRunnablepublic 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 코드 입력실행 방식 (소스 코드)
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 표현식 입력실행 방식 (소스 코드)
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)
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)
// 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 order | DoInOrder | 하위 블록 필요 |
| Do together | DoTogether | 하위 블록 필요 |
| Loop (N times) | LoopNInOrder | end/increment |
| If/Else | IfElseInOrder | condition |
| While | WhileLoopInOrder | condition |
| For each in order | ForEachInOrder | list |
| For all together | ForEachTogether | list |
| Script | ScriptResponse | script 필드 |
| Script defined | ScriptDefinedResponse | script 필드 |
| Wait | Wait | duration |
| Comment | Comment | (실행 안 됨) |
Print | text |
저장/휘발 구분
| 편집 위치 | 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)
방향
| Name | Java Source |
|---|---|
left | Direction.LEFT |
right | Direction.RIGHT |
up | Direction.UP |
down | Direction.DOWN |
forward | Direction.FORWARD |
back / backward | Direction.BACKWARD |
차원
| Name | Java Source |
|---|---|
leftToRight | Dimension.LEFT_TO_RIGHT |
topToBottom | Dimension.TOP_TO_BOTTOM |
frontToBack | Dimension.FRONT_TO_BACK |
all | Dimension.ALL |
HowMuch (실행 범위)
| Name | 의미 |
|---|---|
instance | 이 Element만 |
instanceAndParts | Element + parts |
instanceAndAllDescendants | Element + 모든 하위 요소 |
애니메이션 스타일 (이징)
| Name | Java Source | 설명 |
|---|---|---|
abruptly | LINEAR | 이징 없음 |
beginGently | SLOW_IN | 시작만 느리게 |
endGently | SLOW_OUT | 끝만 느리게 |
gently | SLOW_IN_OUT | 시작과 끝 모두 느리게 (기본값) |
색상
edu.cmu.cs.stage3.alice.scenegraph.Color의 static 필드:
| Color | |||
|---|---|---|---|
red | pink | orange | yellow |
green | blue | purple | brown |
white | lightGray | gray | darkGray |
black | cyan | magenta |
채우기 / 쉐이딩 스타일
| 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.74alice.dot(v, v2) # 32alice.cross(v, v2) # Vec3(-3, 6, -3)센서 (__init__.py)
키보드
kb = alice.keyboardkb.isKeyPressed(kb.VK_SPACE) # 현재 키 상태kb.getKeyNames() # 키 이름 목록모든 java.awt.event.KeyEvent.VK_* 상수 사용 가능: VK_SPACE, VK_ENTER, VK_A … VK_Z, VK_0 … VK_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-703def 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-173def IfElseInOrder(condition, ifAnimation, elseAnimation=None): a = IfElseInOrder() a.addComponentResponse(ifAnimation) #todo ← elseAnimation 파라미터를 전혀 사용하지 않음 ...elseAnimation 파라미터는 받지만 내부적으로 무시됨.
버그 3: LoopNInOrder = None
# animations.py 184-185def 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 용 VariablePinch Gloves (pinchgloves.py)
import pinchglovespinchgloves.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 <|-- PySandboxJava 파일 인벤토리
Scripting API (인터페이스)
경로: src/main/java/edu/cmu/cs/stage3/alice/scripting/
| 파일 | 역할 |
|---|---|
ScriptingFactory.java | 인터프리터 생성/해제, I/O 리디렉션 |
Interpreter.java | compile, eval, exec, start, stop |
Code.java | 컴파일된 스크립트 핸들 |
CompileType.java | EVAL / EXEC_SINGLE / EXEC_MULTIPLE |
Jython 구현
경로: src/main/java/edu/cmu/cs/stage3/alice/scripting/jython/
| 파일 | 역할 | 특징 |
|---|---|---|
ScriptingFactory.java | Jython 초기화: PySystemState.initialize, alice/init.py 로드 | builtins에 로드 |
Interpreter.java | compile: Py.compile_flags → 마지막 인자 null | ⚠️ NPE 원인 |
Code.java | PyCode + CompileType 래핑 | |
Namespace.java | PyStringMap 상속, 4단계 이름 해석 | java2py로 Element → PyElement 변환 |
PyElement.java | PyInstance 상속, findattr_ex 4단계 | child → property → _prefix → reflection |
PySandbox.java | PyElement 상속, setattr 오버라이드 | 변수명 일치 시 variable.value.set() |
Alice Core 통합
경로: src/main/java/edu/cmu/cs/stage3/alice/core/
| 파일 | 역할 |
|---|---|
World.java | ScriptingFactory 소유, compile/eval/exec 위임, start()에서 script 1회 실행 |
Element.java | compile/eval/exec을 getWorld()로 위임, getChildNamedIgnoreCase / getPropertyNamedIgnoreCase |
Sandbox.java | ScriptProperty 보유, behavior 스케줄, lookup() 이름 해석 |
경로: src/main/java/edu/cmu/cs/stage3/alice/core/property/
| 파일 | 역할 |
|---|---|
ScriptProperty.java | StringProperty 상속, Code 캐싱, .py 파일 저장/로드 |
경로: src/main/java/edu/cmu/cs/stage3/alice/core/responses/
| 파일 | 역할 |
|---|---|
ScriptResponse.java | update(t) → exec(EXEC_MULTIPLE) 매 프레임 |
ScriptDefinedResponse.java | prologue(t) → eval(EVAL), 위임 패턴 |
저작 도구 (UI)
경로: src/main/java/edu/cmu/cs/stage3/alice/authoringtool/
| 파일 | 역할 |
|---|---|
editors/scripteditor/ScriptEditor.java | ScriptEditor 전체 |
util/ScriptEditorPane.java | Perform All/Selected, undo/redo |
util/ScriptScratchPad.java | 임시 메모장 |
util/ScriptComboWidget.java | 한 줄 실행 콤보박스 |
util/ElementPopupUtilities.java | EditScriptRunnable |
Element.s_classnameMap (전체 목록)
Element.java static initializer에서 확인된 클래스명 매핑 (이전 버전 → 현재):
// 이름이 바뀐 Response 클래스들ConditionalLoopSequentialResponse → WhileLoopInOrderConditionalSequentialResponse → IfElseInOrderCountLoopSequentialResponse → LoopNInOrderForEachInListSequentialResponse → ForEachOrientationAnimation → ForwardVectorAnimationParallelForEachInListSequentialResponse → ForEachTogetherParallelResponse → DoTogetherProxyForScriptDefinedResponse → ScriptDefinedResponseSequentialForEachInListSequentialResponse → ForEachInOrderSequentialResponse → DoInOrderMetaResponse → CallToUserDefinedResponse
// 내비게이션KeyboardNavigationBehavior → KeyboardNavigationBehaviorMouseNavigationBehavior → MouseLookingBehavior
// 포즈Pose → PosePoseAnimation → PoseAnimationKeyMapping → 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:#08aelementCountHint.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 직렬화.
- 자세한 스키마: 08-element-data-xml.md
하위 디렉토리 규칙
| 종류 | 디렉토리명 | 예 |
|---|---|---|
| named child | child.name 값 | camera, 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 CD4. 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 loaded5. ZIP 스토러 비교
| 특성 | ZipTreeStorer | ZipFileTreeStorer |
|---|---|---|
| 쓰기 방식 | 순차 ZipOutputStream | RandomAccessFile |
| in-place | ❌ | ✅ |
| getKeepKey | null | 파일 기반 |
| 사용처 | 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:#a603. 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의 정보는GalleryViewerUI에 표시되는 용도로만 사용된다.
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) |
|---|---|---|
| 루트 클래스 | World | Model |
galleryData.xml | 없음 | 있음 |
script.py | 있음 (빈 파일) | 없음 |
thumbnail.png | 옵션 (설정) | 필수 |
| 저장 시 스토러 | ZipFileTreeStorer (in-place) | ZipTreeStorer (순차) |
| 편집 가능 | ✅ | ❌ |
5. a2c가 갤러리에서 동작하는 방식
- Alice 실행 >
GalleryViewer가Required/gallery/디렉토리 스캔 - 각
.a2c파일에 대해LocalGalleryObject생성 thumbnail.png- 썸네일,galleryData.xml- 이름/크기/파트 수 표시- 사용자가 드래그하여 월드에 가져올 때:
Element.load(a2cFile, externalRoot)> Model 인스턴스 생성 - “Save Character”는 원본 a2c를 변경하지 않고 새
.a2c를 만듦
6. a2c와 스크립트
a2c 자체에는 Alice UI에서 직접 코드를 추가할 수 없다. 하지만 월드로 가져온 후에는 일반 World 오브젝트처럼 취급되므로 ScriptResponse 블록 등으로 Script를 작성할 수 있다.
# 월드에 가져온 bopper 조작bopper.color = alice.redalice.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">| 속성 | 설명 |
|---|---|
class | Element의 FQCN (Fully Qualified Class Name) |
name | Element의 name Property 값 |
version | 파일 포맷 버전 (현재 2.001 또는 2.2) |
class 매핑: Element.s_classnameMap에 오래된/별칭 클래스명을 최신 클래스로 매핑.
// Element.java static initializers_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.Stringname은 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 | 타입 | 설명 |
|---|---|---|
script | File 참조 | 월드 스크립트 |
responses | Array | 실행 가능한 Response |
behaviors | Array | 등록된 Behavior |
variables | Array | 전역 변수 |
questions | Array | 질문(Question) |
textureMaps | Array | 텍스처 맵 |
data | Dictionary | AuthoringTool 설정 |
Response (ScriptResponse, UserDefinedResponse 등)
| Property | 타입 | 설명 |
|---|---|---|
script | File 참조 | ScriptProperty -> .py 파일 |
duration | Double | 실행 지속 시간 (초) |
isCommentedOut | Boolean | 주석 처리 여부 |
componentResponses | Array | 자식 Response들 |
requiredFormalParameters | Array | 메서드 파라미터 |
localVariables | Array | 지역 변수 |
Behavior (WorldStartBehavior, KeyEventBehavior 등)
| Property | 타입 | 설명 |
|---|---|---|
isEnabled | Boolean | 활성화 여부 (기본 true) |
details | Array | 상세 정보 (키 코드 등) |
triggerResponse | InternalReference | 트리거할 Response |
multipleRuntimeResponsePolicy | Enum | 중복 실행 정책 |
Model
| Property | 타입 | 설명 |
|---|---|---|
color | Color | 모델 색상 |
opacity | Double | 불투명도 |
fillingStyle | Enum | 채우기 스타일 |
visualScale | Matrix4 | 변환 |
diffuseColorMap | InternalReference | 텍스처 참조 |
Variable
| Property | 타입 | 설명 |
|---|---|---|
value | Value (any) | 변수 값 |
valueClass | Class | 값의 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 | 텍스트 노드 |
NumberProperty | class 속성 + 텍스트 노드 |
BooleanProperty | true / false |
ColorProperty | <red>/<green>/<blue>/<alpha> |
Matrix44Property | <row>/<item> |
DictionaryProperty | <entry>/<key>/<value> |
ElementArrayProperty | componentClass + <item> |
ClassProperty | FQCN 문자열 |
EnumProperty | PackagePath[ENUM_NAME] |
ScriptProperty | java.io.File[filename.py] |
ValueProperty | 값 타입에 따라 적절한 방식 선택 |
Alice 2 - 스크립트가 파일에 저장/로드되는 방식
대상:
ScriptProperty직렬화 메커니즘과 4가지 스크립트 입력면의 저장 차이 출처:ScriptProperty.java,ScriptResponse.java,ScriptDefinedResponse.java분석 기준: alice2 source + Alice 2.6.1 배포본.
1. ScriptProperty 개요
ScriptProperty는 StringProperty의 하위 클래스로, 문자열 스크립트를 별도의 .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_associatedFileKey | ZIP 내 .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.xmlprotected 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가지 스크립트 입력면 저장 위치 비교
| 입력면 | Property | ZIP 내 파일 경로 |
|---|---|---|
| Sandbox.script (World) | ScriptProperty | /script.py |
| ScriptResponse.script | ScriptProperty | /behaviorN/__UnnamedN__/script.py |
| ScriptDefinedResponse.script | ScriptProperty | /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 →| RP25. 코드 컴파일과 캐싱
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;}- script 빈 문자열 →
m_code = null반환 m_code == null→owner.compile()으로 Jython 컴파일- 결과 캐시,
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는 별도 디렉토리에 저장:
<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 = 100Behavior와 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_classnameMap에 SoundAction → SoundResponse 매핑이 없음.
// Element.java static initializer — 확인된 매핑 목록// SoundAction → SoundResponse 매핑 없음!a2w ZIP 내부에서 확인하는 방법
- a2w를 ZIP으로 열기
- 모든
elementData.xml에서 다음 문자열 검색:class="edu.cmu.cs.stage3.alice.core.responses.SoundAction" - 발견 시
SoundAction→SoundResponse로 수동 변경
수동 수정
<!-- 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:#a602. 팔레트 초기화 (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 클래스 | knownPropertyValues | desiredProperties |
|---|---|---|---|
| DoInOrder | DoInOrder.class | null | null |
| DoTogether | DoTogether.class | null | null |
| If/Else | IfElseInOrder.class | null | {"condition"} |
| Loop N times | LoopNInOrder.class | null | {"end"} |
| While | WhileLoopInOrder.class | null | {"condition"} |
| ForEach | ForEachInOrder.class | null | {"list"} |
| script | ScriptResponse.class | null | null |
| script defined | ScriptDefinedResponse.class | null | null |
| Comment | Comment.class | {"text", "No comment"} | null |
Print.class | null | null |
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의 핵심 드래그 코드:
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 | 용도 | 반환 객체 |
|---|---|---|
responsePrototypeReferenceFlavor | ResponseEditor 전용 드롭 | 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 end6. 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. 키 포인트 요약
- 팔레트의 각 블록은
DnDGroupingPanel+ResponsePrototype쌍이다. - 드래그 시
ResponsePrototypeReferenceTransferable이ResponsePrototype객체를 전달한다. - 드롭 시
CompositeComponentResponsePanel.drop()이responsePrototypeReferenceFlavor로 Prototype을 추출한다. ElementPrototype.createNewElement()가Class.newInstance()로 Element를 생성하고, knownPropertyValues를 설정한다.performDrop()이 Element 트리의ObjectArrayProperty에 새 Element를 추가한다.- 복사/이동/신규 생성 모두 동일한
performDrop()을 통해 처리된다. ForEach/LoopN등 특수 블록은 자동으로 index/item 변수를 생성한다.- Undo/Redo는
startCompound()/stopCompound()로 그룹핑된다.
ScriptResponse와 ScriptDefinedResponse의 차이는
createNewElement()시점에는 없다.
둘 다 동일한 패턴으로new ScriptResponse()/new ScriptDefinedResponse()인스턴스만 생성된다.
차이는 런타임에CompileType.EXEC_MULTIPLEvsCompileType.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_classnameMap에 SoundAction → 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 연결이 깨짐.
해결:
- “Yes” 눌러 로딩 계속
- Alice UI에서 해당 behavior의 triggerResponse 속성 재할당
- 또는 ZIP 내 elementData.xml에서 criterionClass 참조 수정
2. 수정된 월드가 로딩 시 Reference Warning
현상: 팀원이 저장한 a2w를 열면 수많은 unable to resolve references 경고가 뜸
원인: ZIP 내 unnamed child 디렉토리명(__UnnamedN__)의 인덱스가 elementData.xml의 참조와 불일치.
블록 편집기에서 복사/이동/삭제 시 내부 참조가 깨질 수 있음.
해결:
- “Yes” 누르고 로딩 (모든 참조 None 처리)
- Alice UI에서 behavior들의
triggerResponse를 다시 연결 - 저장 전에 모든 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 했는데 레이블 없이 숫자만 뜸
원인 & 해결:
- Script는 동작하지 않음 — Alice 2.6.1 Jython NPE 문제 (01-architecture.md 참고)
- 한글이 안 보임 — Alice 2의 Text3D/SayAnimation은 CJK 글리프를 지원하지 않음
- 레이블 + 값 표시 —
DoTogether로 여러 SayAnimation 블록 연결:
DoTogether: car → say "Score: " (0.1초) car → say score (0.1초) car → say " | Wrong: " (0.1초) car → say wrongCount (0.1초)- Text3D 오브젝트 사용 — 미리
ScoreText등의 Text3D를 배치해두고textProperty를 블록의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) 글리프를 지원하지 않음. 기본 폰트에 한글 글리프가 없음.
해결:
- 모든 사용자 텍스트를 영어로 작성
정답: 5→Correct: 5/Score: 5오답: 2→Wrong: 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수정 팁:
- ZIP 파일 바로 열기 → 7-Zip 등으로 열고 텍스트 파일 직접 편집
elementData.xml에서<property name="class">값 수정으로 클래스명 변경 가능- 변수 값:
<property class="java.lang.Double" name="value">5.0</property> - 참조 복구:
<property criterionClass="..." name="triggerResponse">behavior0.__Unnamed1__</property> - 저장 후 Alice로 로드 확인
10. “enableScripting” 메뉴 안 보임
현상: 우클릭 메뉴에 “Edit Script”가 없음
원인: Alice 설정에서 enableScripting = false
해결: Alice 설치 디렉토리에서 설정 파일 확인
Required/아래 설정에서enableScripting=true로 변경- 또는
Alice.ini등 설정 파일 수정