Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ __pycache__/
.codex
chat
task-*
*.md
.gitignore
*.png

# Unity generated state
unity/**/Library/
unity/**/Temp/
unity/**/Obj/
unity/**/Build/
unity/**/Builds/
unity/**/Logs/
unity/**/UserSettings/
unity/**/*.csproj
unity/**/*.sln
unity/**/*.user
unity/**/*.pidb
unity/**/*.booproj
unity/**/*.svd
unity/**/TestResults.xml
unity/**/PlayModeTestResults.xml
unity/**/*.log
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
* `final seal` compile 결과 표시
* JSON 로그 export
* 문서 상태 동기화 검증
* Unity 기반 `Magic Exam Hall` 5층 월드 캐스팅 플레이 루프

## 요구 사항

웹 프로토타입:

* Node.js `20.x` 이상
* npm `10.x` 이상

Expand All @@ -25,6 +28,11 @@ node -v
npm -v
```

Unity 플레이어블:

* Unity `6000.3.14f1`
* 유효한 Unity Editor 라이선스

## 빠른 시작

처음 clone 후 가장 먼저 할 일:
Expand All @@ -43,6 +51,18 @@ npm run dev

Vite가 출력하는 로컬 주소를 브라우저에서 열면 됩니다. 보통은 `http://localhost:5173` 입니다.

Unity 플레이어블 실행:

```text
unity/MagicExamHall
```

1. Unity Hub 또는 Unity Editor에서 위 프로젝트 폴더를 엽니다.
2. `Assets/Scenes/MagicExamHall.unity`를 엽니다.
3. Play를 누릅니다.

조작은 WASD/방향키 이동, 우클릭 hold/release 월드 드로잉입니다. 기본 플레이에서는 별도 입력 패널이나 `마법 시전` 버튼을 사용하지 않습니다.

## 초기 검증 튜토리얼

처음 받았을 때는 아래 순서로 확인하면 됩니다.
Expand Down Expand Up @@ -196,6 +216,14 @@ npm run test:watch # Vitest watch
npm run validate:docs # docs 상태/의존성 검증
```

Unity 검증 명령 예시:

```powershell
& 'C:\Program Files\Unity\Hub\Editor\6000.3.14f1\Editor\Unity.com' -batchmode -quit -projectPath 'C:\Users\silve\source\repos\magic\unity\MagicExamHall' -executeMethod MagicExamHall.Editor.MagicExamHallSceneBuilder.BuildAll
& 'C:\Program Files\Unity\Hub\Editor\6000.3.14f1\Editor\Unity.com' -batchmode -projectPath 'C:\Users\silve\source\repos\magic\unity\MagicExamHall' -runTests -testPlatform editmode -testResults 'C:\Users\silve\source\repos\magic\unity\MagicExamHall\TestResults.xml'
& 'C:\Program Files\Unity\Hub\Editor\6000.3.14f1\Editor\Unity.com' -batchmode -projectPath 'C:\Users\silve\source\repos\magic\unity\MagicExamHall' -runTests -testPlatform playmode -testResults 'C:\Users\silve\source\repos\magic\unity\MagicExamHall\PlayModeTestResults.xml'
```

## 디렉토리 구조

```text
Expand All @@ -204,6 +232,7 @@ tests/ Vitest 테스트
scripts/ 문서 검증 스크립트
docs/ 방향/큐/task 문서
chat/ 원본 논의 보관소
unity/ Unity Magic Exam Hall 플레이어블
```

## 문서 읽기 순서
Expand All @@ -230,3 +259,11 @@ npm ci
```bash
npm run validate:docs
```

Unity batchmode에서 아래 메시지가 보이면 코드 컴파일 실패가 아니라 라이선스 초기화 문제입니다.

```text
No valid Unity Editor license found. Please activate your license.
```

로그 끝의 `abort_threads: Failed aborting id ... mono_thread_manage will ignore it` 메시지는 Unity/Mono 종료 과정에서 남는 노이즈일 수 있습니다. 같은 로그 안에 `Exception`, `CS#### error`, `Test run failed`가 함께 있는지 먼저 확인하세요.
14 changes: 14 additions & 0 deletions unity/MagicExamHall/Assets/MagicExamHall.Runtime.asmdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "MagicExamHall.Runtime",
"rootNamespace": "MagicExamHall",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions unity/MagicExamHall/Assets/MagicExamHall.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions unity/MagicExamHall/Assets/MagicExamHall/Editor.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "MagicExamHall.Editor",
"rootNamespace": "MagicExamHall.Editor",
"references": [
"MagicExamHall.Runtime"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using MagicExamHall;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

namespace MagicExamHall.Editor
{
public static class MagicExamHallSceneBuilder
{
private const string ScenePath = "Assets/Scenes/MagicExamHall.unity";
private const string PrefabFolder = "Assets/MagicExamHall/Prefabs";
private const string ResourcesFolder = "Assets/MagicExamHall/Resources";
private const string MaterialFolder = "Assets/MagicExamHall/Resources/MagicExamHallMaterials";

[MenuItem("Magic Exam Hall/Rebuild Demo Scene")]
public static void BuildAll()
{
EnsureFolders();
EnsureMaterials();

var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
scene.name = "MagicExamHall";

var camera = CreateCamera();
CreateFloor();
var player = CreatePlayer();
var canvas = CreateCanvas();
CreateEventSystem();
CreateController(camera, player.transform, canvas);
SavePrefabs(player);

EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene, ScenePath);
EditorBuildSettings.scenes = new[] { new EditorBuildSettingsScene(ScenePath, true) };
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"Magic Exam Hall scene rebuilt at {ScenePath}");
}

private static void EnsureFolders()
{
if (!AssetDatabase.IsValidFolder("Assets/Scenes"))
{
AssetDatabase.CreateFolder("Assets", "Scenes");
}

if (!AssetDatabase.IsValidFolder("Assets/MagicExamHall"))
{
AssetDatabase.CreateFolder("Assets", "MagicExamHall");
}

if (!AssetDatabase.IsValidFolder(PrefabFolder))
{
AssetDatabase.CreateFolder("Assets/MagicExamHall", "Prefabs");
}

if (!AssetDatabase.IsValidFolder(ResourcesFolder))
{
AssetDatabase.CreateFolder("Assets/MagicExamHall", "Resources");
}

if (!AssetDatabase.IsValidFolder(MaterialFolder))
{
AssetDatabase.CreateFolder(ResourcesFolder, "MagicExamHallMaterials");
}
}

private static void EnsureMaterials()
{
CreateOrUpdateMaterial($"{MaterialFolder}/PixelSpriteDefault.mat", "Sprites/Default");
CreateOrUpdateMaterial($"{MaterialFolder}/PixelUIDefault.mat", "UI/Default");
}

private static void CreateOrUpdateMaterial(string path, string shaderName)
{
var shader = Shader.Find(shaderName);
if (shader == null)
{
Debug.LogWarning($"Could not find shader {shaderName}; material {path} was not generated.");
return;
}

var material = AssetDatabase.LoadAssetAtPath<Material>(path);
if (material == null)
{
material = new Material(shader);
AssetDatabase.CreateAsset(material, path);
}
else
{
material.shader = shader;
EditorUtility.SetDirty(material);
}
}

private static Camera CreateCamera()
{
var cameraObject = new GameObject("Main Camera");
cameraObject.tag = "MainCamera";
cameraObject.transform.position = new Vector3(0f, 0f, -10f);
var camera = cameraObject.AddComponent<Camera>();
camera.orthographic = true;
camera.orthographicSize = 6.2f;
camera.backgroundColor = new Color(0.06f, 0.08f, 0.11f);
return camera;
}

private static void CreateFloor()
{
CreatePixelObject("Stone Tile Floor", Vector2.zero, Vector3.one, PixelSpriteKind.FloorTile,
new Color(0.16f, 0.18f, 0.23f), new Color(0.10f, 0.12f, 0.16f), -7, true, new Vector2(16.4f, 10f));
CreatePixelObject("North Carved Wall", new Vector2(0f, 4.95f), Vector3.one, PixelSpriteKind.WallTrim,
new Color(0.22f, 0.20f, 0.27f), new Color(0.63f, 0.50f, 0.23f), -4, true, new Vector2(16.4f, 1.15f));
CreatePixelObject("South Carved Wall", new Vector2(0f, -4.95f), Vector3.one, PixelSpriteKind.WallTrim,
new Color(0.18f, 0.17f, 0.22f), new Color(0.50f, 0.40f, 0.20f), -4, true, new Vector2(16.4f, 0.8f));
CreatePixelObject("Center Runner", new Vector2(0f, 0.15f), Vector3.one, PixelSpriteKind.Rug,
new Color(0.55f, 0.10f, 0.17f), new Color(0.95f, 0.69f, 0.26f), -5, true, new Vector2(2.2f, 7.6f));
CreatePixelObject("West Runner", new Vector2(-4.25f, 0f), Vector3.one, PixelSpriteKind.Rug,
new Color(0.14f, 0.34f, 0.44f), new Color(0.80f, 0.65f, 0.32f), -5, true, new Vector2(1.45f, 4.8f));
CreatePixelObject("East Runner", new Vector2(4.25f, 0f), Vector3.one, PixelSpriteKind.Rug,
new Color(0.14f, 0.34f, 0.44f), new Color(0.80f, 0.65f, 0.32f), -5, true, new Vector2(1.45f, 4.8f));

CreateProp("West Bookcase", new Vector2(-7.25f, 1.25f), new Vector3(1.25f, 1.25f, 1f), PixelSpriteKind.Bookshelf,
new Color(0.42f, 0.23f, 0.12f), new Color(0.42f, 0.80f, 0.88f), -1);
CreateProp("East Bookcase", new Vector2(7.25f, 1.25f), new Vector3(1.25f, 1.25f, 1f), PixelSpriteKind.Bookshelf,
new Color(0.42f, 0.23f, 0.12f), new Color(0.68f, 0.36f, 0.86f), -1);
CreateProp("Northwest Candelabra", new Vector2(-6.85f, 3.65f), Vector3.one * 0.9f, PixelSpriteKind.Candle,
new Color(0.63f, 0.57f, 0.44f), new Color(1f, 0.56f, 0.15f), 2);
CreateProp("Northeast Candelabra", new Vector2(6.85f, 3.65f), Vector3.one * 0.9f, PixelSpriteKind.Candle,
new Color(0.63f, 0.57f, 0.44f), new Color(1f, 0.56f, 0.15f), 2);
}

private static GameObject CreateProp(string name, Vector2 position, Vector3 scale, PixelSpriteKind kind, Color primary, Color secondary, int sortingOrder)
{
return CreatePixelObject(name, position, scale, kind, primary, secondary, sortingOrder, false, Vector2.one);
}

private static GameObject CreatePixelObject(string name, Vector2 position, Vector3 scale, PixelSpriteKind kind, Color primary, Color secondary, int sortingOrder, bool tiled, Vector2 tiledSize)
{
var body = new GameObject(name);
body.transform.position = position;
body.transform.localScale = scale;
body.AddComponent<SpriteRenderer>();
var sprite = body.AddComponent<PixelSpriteView>();
sprite.kind = kind;
sprite.primary = primary;
sprite.secondary = secondary;
sprite.sortingOrder = sortingOrder;
sprite.tiled = tiled;
sprite.tiledSize = tiledSize;
return body;
}

private static GameObject CreatePlayer()
{
var player = new GameObject("Apprentice");
player.transform.position = new Vector3(0f, -4.1f, 0f);
player.transform.localScale = Vector3.one * 0.78f;
player.AddComponent<SpriteRenderer>();
var sprite = player.AddComponent<PixelSpriteView>();
sprite.kind = PixelSpriteKind.Player;
sprite.primary = new Color(0.95f, 0.92f, 0.78f);
sprite.secondary = new Color(0.28f, 0.62f, 0.96f);
sprite.sortingOrder = 4;
return player;
}

private static Canvas CreateCanvas()
{
var canvasObject = new GameObject("Exam Canvas");
canvasObject.AddComponent<RectTransform>();
var canvas = canvasObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
var scaler = canvasObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1280, 720);
canvasObject.AddComponent<GraphicRaycaster>();
return canvas;
}

private static void CreateEventSystem()
{
var eventSystem = new GameObject("EventSystem");
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}

private static void CreateController(Camera camera, Transform player, Canvas canvas)
{
var controllerObject = new GameObject("Exam Game Controller");
var controller = controllerObject.AddComponent<ExamGameController>();
controller.mainCamera = camera;
controller.player = player;
controller.canvas = canvas;
var drawing = controllerObject.AddComponent<WorldDrawingController>();
drawing.mainCamera = camera;
}

private static void SavePrefabs(GameObject player)
{
PrefabUtility.SaveAsPrefabAsset(player, $"{PrefabFolder}/Apprentice.prefab");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions unity/MagicExamHall/Assets/MagicExamHall/Prefabs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading