[개발 일지] 6일차: 생동감 있는 전투와 개발 생산성의 조화 (with Antigravity)
*본 개발 일지는 Google의 차세대 AI 에이전트 **'Antigravity'*와 함께 작성되었습니다.
🏗️ 6일차 개발 목표
오늘은 시스템적인 기능을 넘어 게임의 **'보여지는 맛'**을 살리는 데 집중했습니다. 밋밋하게 움직이던 유닛에 숨결을 불어넣고, 이를 효율적으로 다듬기 위한 전용 도구들을 제작했습니다.
- 애니메이션 시스템 연동 (Visuals)
- 개발자를 위한 도구 구축 (Dev Tools)
- 연출의 MSG: 카메라 액션 (Polish)
1. 애니메이션 시스템 연동 (Visuals)
🎭 상태 머신(State Machine) 구축
단순히 이동하고 멈추는 것을 넘어, 유니티 Mecanim을 활용해 정교한 상태 전이를 구현했습니다.
- Smart Triggers: 공격(Attack)과 마법(Cast)을 구분했습니다. 코드가 스킬 타입(Heal/Buff)을 감지하여, 마법사는 이제 지팡이로 때리는 대신 주문을 외우는 고유 모션을 취합니다.
- Flow Control: Idle <-> Run 자동 전환 및 액션 종료 후 복귀 로직을 완성했습니다.
⚔️ 공방의 리얼리티: 반격(Counter-Attack)
가장 큰 변화는 '반격' 메커니즘의 시각화입니다.
Before: A 공격 -> B 체력 감소 (멀뚱멀뚱) -> 끝
After: A 공격 -> B 피격 -> B가 A를 향해 몸을 돌림 -> B의 반격 모션 -> A 피격
코드를 재사용 가능한 구조(PerformAttackAnimation)로 리팩토링하여, 복잡한 공방 순서를 깔끔하게 정리했습니다.
2. 개발자를 위한 도구 (Dev Tools)
게임을 만드는 과정을 '게임'처럼 쾌적하게 만들기 위해 두 가지 도구를 직접 개발했습니다.
🎬 Scene Switcher (씬 스위처)
프로젝트가 커지면서 씬 이동이 번거로워졌습니다.
- 해결: 상단 메뉴에 전용 툴을 달아, 클릭 한 번으로 씬을 오가도록 만들었습니다.
- 디테일: Play Mode일 때 실수로 이동하지 않도록 안전장치까지 꼼꼼히 챙겼습니다.
using UnityEngine;
using UnityEditor;
using UnityEditor.SceneManagement;
using System.IO;
namespace SRPG.Editor
{
public class SceneSwitcherWindow : EditorWindow
{
[MenuItem("Tools/Scene Switcher")]
public static void ShowWindow()
{
GetWindow<SceneSwitcherWindow>("Scene Switcher");
}
private Vector2 scrollPos;
private void OnGUI()
{
GUILayout.Label("Project Scenes", EditorStyles.boldLabel);
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
string[] guids = AssetDatabase.FindAssets("t:Scene");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
string sceneName = Path.GetFileNameWithoutExtension(path);
// Filter out non-project scenes if necessary (e.g., packages)
if (!path.StartsWith("Assets/")) continue;
if (GUILayout.Button(sceneName))
{
OpenScene(path);
}
}
EditorGUILayout.EndScrollView();
}
private void OpenScene(string scenePath)
{
if (EditorApplication.isPlaying)
{
if (EditorUtility.DisplayDialog("Scene Switcher", "Play Mode를 종료하고 씬을 이동하시겠습니까?", "예", "아니오"))
{
EditorApplication.isPlaying = false;
}
else
{
return;
}
}
if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
EditorSceneManager.OpenScene(scenePath);
}
}
}
}
⏱️ Animation Tester (애니메이션 테스터)
- 문제: "공격 모션이 칼에 닿기도 전에 데미지가 뜨네?" -> 수정 -> 빌드 -> 확인 -> 반복...
- 해결: 전용 테스트 씬에서 슬라이더로 타격 타이밍(Delay)을 0.01초 단위로 조절하고, 버튼 하나로 데이터에 즉시 저장하는 툴을 구축했습니다. 이제 감각적인 타격감을 1분 만에 깎을 수 있습니다.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using SRPG.Character;
using SRPG.Data;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace SRPG.Tools
{
public class AnimationTestManager : MonoBehaviour
{
[Header("Test Settings")]
public List<UnitData> unitDataList;
public GameObject targetDummyPrefab;
[Header("Current Test")]
public int selectedUnitIndex = 0;
public UnitData currentUnitData;
public Unit currentUnitInstance;
public Unit dummyInstance;
[Header("Timing Adjustment")]
[Range(0f, 2.0f)]
public float testDelay = 0.4f;
private void Start()
{
if (targetDummyPrefab != null)
{
GameObject dummyObj = Instantiate(targetDummyPrefab, new Vector3(2, 0, 0), Quaternion.identity);
dummyInstance = dummyObj.GetComponent<Unit>();
if (dummyInstance == null) dummyInstance = dummyObj.AddComponent<Unit>();
// Minimal setup for dummy
dummyInstance.unitName = "Dummy";
dummyInstance.stats = new UnitStats();
dummyInstance.stats.maxHP = 9999;
dummyInstance.stats.currentHP = 9999;
}
SpawnSelectedUnit();
}
public void SpawnSelectedUnit()
{
if (currentUnitInstance != null)
{
#if UNITY_EDITOR
if (Selection.activeGameObject == currentUnitInstance.gameObject)
{
Selection.activeGameObject = null;
}
#endif
Destroy(currentUnitInstance.gameObject);
}
if (unitDataList != null && unitDataList.Count > 0)
{
selectedUnitIndex = Mathf.Clamp(selectedUnitIndex, 0, unitDataList.Count - 1);
currentUnitData = unitDataList[selectedUnitIndex];
testDelay = currentUnitData.attackHitDelay;
if (currentUnitData.unitPrefab != null)
{
GameObject unitObj = Instantiate(currentUnitData.unitPrefab, Vector3.zero, Quaternion.identity);
currentUnitInstance = unitObj.GetComponent<Unit>();
if (currentUnitInstance != null)
{
currentUnitInstance.Setup(currentUnitData, Vector2Int.zero);
}
}
}
}
public void TestAttack()
{
if (currentUnitInstance == null || dummyInstance == null) return;
currentUnitInstance.transform.position = Vector3.zero;
currentUnitInstance.StartCoroutine(PerformTestAttack());
}
private IEnumerator PerformTestAttack()
{
// Apply temp delay for testing logic (Unit.cs logic might need slight override or just set data)
// But to test "real" logic, we should probably modify the SO in memory or existing Unit instance.
// For true WYSIWYG, let's update the data in memory
if (currentUnitData != null)
{
currentUnitData.attackHitDelay = testDelay;
// Dirty the Unit to ensure it uses fresh data if it cached anything (Unit.cs reads from originData directly currently)
}
yield return currentUnitInstance.StartCoroutine("PerformAttackVisuals", dummyInstance);
}
public void SaveDelayToData()
{
#if UNITY_EDITOR
if (currentUnitData != null)
{
currentUnitData.attackHitDelay = testDelay;
EditorUtility.SetDirty(currentUnitData);
AssetDatabase.SaveAssets();
Debug.Log($"Saved delay {testDelay} to {currentUnitData.name}");
}
#endif
}
private void OnGUI()
{
GUILayout.BeginArea(new Rect(10, 10, 300, 400));
GUILayout.Label("Animation Tester", GUI.skin.box);
if (unitDataList != null)
{
string[] options = new string[unitDataList.Count];
for (int i = 0; i < unitDataList.Count; i++) options[i] = unitDataList[i].name;
int newIndex = EditorGUILayout.Popup("Select Unit", selectedUnitIndex, options);
if (newIndex != selectedUnitIndex)
{
selectedUnitIndex = newIndex;
SpawnSelectedUnit();
}
}
GUILayout.Space(10);
if (GUILayout.Button("Spawn / Reset"))
{
SpawnSelectedUnit();
}
GUILayout.Space(20);
GUILayout.Label($"Hit Delay: {testDelay:F2} sec");
testDelay = GUILayout.HorizontalSlider(testDelay, 0f, 2.0f);
if (GUILayout.Button("Test Attack"))
{
TestAttack();
}
GUILayout.Space(10);
if (GUILayout.Button("Save to Data Asset"))
{
SaveDelayToData();
}
GUILayout.EndArea();
}
}
}
3. 연출의 MSG: 카메라 액션 (Polish)
🎥 Action Zoom
크리티컬이나 스킬 발동 순간, 카메라가 확 줌인되는 연출을 고도화했습니다.
- 단순한 선형 이동(Lerp)이 아니라, Animation Curve를 적용하여 "스윽- 탁!" 하고 꽂히는 쫀득한 손맛을 구현했습니다.
- 개발자가 코드 수정 없이 인스펙터에서 줌인 강도와 시간을 자유롭게 튜닝할 수 있게 되었습니다.
📊 6일차 개발 통계
| 항목 | 내용 |
| 구현된 애니메이션 상태 | 6종 (Idle, Run, Attack, Cast, Hit, Die) |
| 신규 개발 도구 | 2종 (SceneSwitcher, AnimationTester) |
| 리팩토링 | 반격 로직 분리 및 카메라 줌 시스템 고도화 |
| 생산성 향상 | 애니메이션 타이밍 조절 시간 90% 단축 (체감) |
📝 6일차 개발을 마치며
"퀄리티는 반복(Iteration)에서 나오고, 도구는 그 반복을 돕습니다."
오늘은 눈에 보이는 화려함 뒤에, 그것을 지탱하는 **'개발 환경'**을 개선하는 데 많은 시간을 썼습니다. Antigravity가 제안한 Animation Tester 덕분에, 앞으로 추가될 수십 종의 유닛들의 타격감을 쉽고 빠르게 잡아낼 수 있는 기반이 마련되었습니다.
다음 단계: 뼈대와 살이 붙었으니, 이제 **'화려한 옷'**을 입힐 차례입니다. 다음 시간에는 타격감을 극대화할 **'VFX(이펙트) 시스템'**을 연동하고, 실제 전투의 박진감을 높여보겠습니다.
"도구가 편해지면, 개발자는 디테일에 집착할 여유가 생깁니다."
'AI > Unity' 카테고리의 다른 글
| AntiGravity를 이용한 SRPG만들기 8일차 (툰쉐이더 적용) (0) | 2026.01.20 |
|---|---|
| AnityGravity를 이용한 SRPG만들기 7일차 (AI모델 자동익스포터 툴 제작) (0) | 2026.01.20 |
| tripo3d에서 FBX 추출 후 유니티에 적용하기 - 애니메이션 (2). (3) | 2026.01.18 |
| tripo3d에서 FBX 추출 후 유니티에 적용하기. (0) | 2026.01.18 |
| AntiGravity를 이용한 SRPG만들기 5일차 (5) | 2026.01.13 |