안녕하세요! 👋 AI 코딩으로 상용화 레벨의 방치형 RPG를 내 손으로 직접 빌드해 가는 열 번째 시간입니다.
지난 9강에서는 난수(Random) 누적 가중치 기반의 장비 뽑기 시스템을 구축하여, 지정된 확률 테이블에 따라 주인공 머리 위 공간으로 다채로운 등급별 무기 오브젝트를 무작위로 복제 소환해 내는 짜릿한 손맛을 구현했습니다. 하지만 9강 상태 그대로 두면 치명적인 버그와 함께 시스템의 한계가 찾아옵니다. 뽑기 버튼을 눌러 필드에 멋진 무기가 떨어져도, 영웅이 그 무기를 손에 쥐지 못해 외형이 바뀌지 않고, 무기 고유의 강력한 스탯 보너스도 내 진짜 공격력에 전혀 반영되지 않는다는 점입니다. 😢
내 캐릭터가 더 좋은 전설 무기를 뽑았는데도 외형과 스탯이 따로 논다면 유저들이 금방 지루해하고 게임을 지워버리겠죠?
그래서 오늘은 가챠 매니저와 자동 전투 스크립트를 유기적으로 연결하여 1) 무기를 뽑는 즉시 영웅의 손 위치에 무기 도트 이미지(Sprite)를 실시간으로 부착하고, 2) 장착한 무기 고유의 스탯 보너스(공격력, 공속, 이속) 및 "주변의 적들을 설정된 마릿수만큼 한 번에 쓸어버리는 실시간 광역 타격 가산 시스템"을 Cursor AI (sonnet4) 비서와 함께 단 1프레임의 오차도 없이 아주 정밀하게 조립해 보겠습니다. 마우스 조작선을 따라 하나씩 차근차근 출발해 보시죠! 🚀
🖥️ 1) 1단계: Cursor(sonnet4.6)에게 "가챠 무기 외형 장착 및 int 기반 광역 타격 연동" 지시하기
무기 데이터 설정을 쥐고 있는 GachaManager.cs가 새로운 무기를 실시간으로 영웅의 손 컴포넌트에 쥐어주도록 만들고, 전투를 담당하는 PlayerAutoCombat.cs가 그 무기 스펙을 완벽하게 수입하도록 뇌를 동기화해주어야 합니다.
Cursor 프로그램 화면을 켜고, 작업하던 PlayerAutoCombat.cs 파일과 GachaManager.cs 파일을 연달아 활성화합니다.
단축키 Ctrl + I를 눌러 Composer 창을 열고, 아래와 같이 우리 비서에게 명확하게 리팩토링을 지시합니다.
💬 Cursor AI 에이전트 프롬프트:
"우리가 만든 PlayerAutoCombat.cs와 GachaManager.cs 스크립트 간의 장비 장착 스탯 및 다중 공격 연동 구조를 완벽하게 고도화해 줘.
[필수 포함 연동 및 버그 방어 로직]
- GachaManager는 무기가 새로 뽑혀서 장착되는 순간 기존 무기 오브젝트를 지우고 자식으로 새 무기를 Instantiate 한 뒤, 현재 최고 존엄 무기의 최대 타격 수(maxTargets)를 반환하는 public int GetCurrentWeaponMaxTargets() 함수를 완벽히 노출해 줘.
- PlayerAutoCombat은 무기를 휘두르는 순간 가챠 매니저를 실시간 참조하여, 뽑은 무기의 보너스 공격력, 공격 속도 배율, 이동 속도를 내 플레이어 스탯에 100% 실시간 가산되도록 구조를 묶어 줘.
- 다중 타격 시 오브젝트를 실시간으로 담고 비우는 List 수집 방식은 메모리 오염을 만드니 완전히 삭제하고, 오차 없는 원형 반경(multiAttackRadius) 스캔 방식으로 변경해 줘.
- GameObject.FindGameObjectsWithTag("Enemy")로 범위 내 적들을 배열로 긁어모은 뒤 가까운 순서로 정렬하고, 오직 가챠 무기 스펙 정수(maxTargetsCount) 한도만큼만 순수 카운트(attackCount++)하며 즉시 동시 대미지(TakeDamage)를 집행하는 정석 int 연산 구조로 뚫어 줘.
- 가챠를 아직 안 뽑았을 때(인덱스 -1)나 데이터 누수 시 기본 타격 수는 3으로 고정 방화벽을 쳐 줘. 그리고 현재 최종 연산에 반영된 순수 타격 마릿수 스펙을 인스펙터창에서 상시 모니터링할 수 있도록 읽기 전용 변수인 [SerializeField] private int currentMaxTargetsDisplay;를 만들어서 실시간 대입해 줘."
전송을 누르면 sonnet4 비서가 메모리 주소 오염이나 수치 튐 현상을 원천 차단한 완성형 코드를 제출합니다. 확인 후 [Accept] 버튼을 눌러 저장합니다.

💻 2) 2단계: AI가 완성한 고도화 성장 스크립트 분석
Cursor AI가 가독성 높은 주석과 함께 새롭게 빌드해 준 두 핵심 코드의 연동 구조입니다.
[PlayerAutoCombat.cs - 자동 전투 엔진]
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[DisallowMultipleComponent]
[RequireComponent(typeof(Animator))]
public sealed class PlayerAutoCombat : MonoBehaviour
{
private const string EnemyTag = "Enemy";
private static readonly int MoveXHash = Animator.StringToHash("MoveX");
private static readonly int MoveYHash = Animator.StringToHash("MoveY");
private static readonly int IsWalkingHash = Animator.StringToHash("isWalking");
private static readonly int AttackHash = Animator.StringToHash("Attack");
[Header("🏃 1. 이동 및 접근 거리")]
[SerializeField] private float moveSpeed = 3f;
[SerializeField] private float approachRange = 0.7f;
[Header("⚔️ 2. 실제 다중 공격 범위 (int 연산 기반)")]
[SerializeField] [Tooltip("캐릭터 주변 반지름 몇 유닛 안의 적들을 광역 타격할 것인가")] private float multiAttackRadius = 1.6f;
[SerializeField] private float attackInterval = 1f;
[SerializeField] private float attackDuration = 0.5f;
[Header("📊 3. 실시간 스탯 디버그 창")]
// 🌟 가챠 무기 등급에 따라 '최대 몇 마리'까지 동시 타격 가능한지 눈으로 실시간 모니터링하는 안전 칸!
[SerializeField] private int currentMaxTargetsDisplay;
[Header("상태 (디버그용)")]
[SerializeField] private Transform mainTargetEnemy;
[SerializeField] private bool isAttacking;
private Animator _animator;
private Coroutine _attackCoroutine;
private Vector2 _lastFacing = Vector2.down;
private GachaManager _gachaManager;
private void Awake()
{
_animator = GetComponent<Animator>();
_gachaManager = FindObjectOfType<GachaManager>();
}
private void Update()
{
if (mainTargetEnemy == null || mainTargetEnemy.gameObject.activeInHierarchy == false || IsTargetDead(mainTargetEnemy))
{
mainTargetEnemy = FindClosestEnemyByTag();
}
if (mainTargetEnemy == null)
{
StopAttackCoroutine();
SetIdleAnimation();
return;
}
if (isAttacking)
{
ApplyFacing(CalcFacing(mainTargetEnemy));
return;
}
var toEnemy = (Vector2)mainTargetEnemy.position - (Vector2)transform.position;
var distance = toEnemy.magnitude;
var facing = GetNormalizedFacing(toEnemy, distance);
if (distance <= approachRange)
{
SetWalking(false);
ApplyFacing(facing);
StartAttackCoroutine();
return;
}
StopAttackCoroutine();
SetWalking(true);
ApplyFacing(facing);
MoveTowardEnemy(toEnemy, distance);
}
private bool IsTargetDead(Transform target)
{
if (target == null) return true;
var hp = target.GetComponent<MonsterHP>();
return hp != null && hp.IsDead;
}
private Transform FindClosestEnemyByTag()
{
var enemies = GameObject.FindGameObjectsWithTag(EnemyTag);
Transform closest = null;
var closestSqrDist = float.MaxValue;
var myPos = (Vector2)transform.position;
foreach (var enemy in enemies)
{
if (enemy == null || enemy.activeInHierarchy == false) continue;
var hp = enemy.GetComponent<MonsterHP>();
if (hp != null && hp.IsDead) continue;
var sqrDist = ((Vector2)enemy.transform.position - myPos).sqrMagnitude;
if (sqrDist < closestSqrDist)
{
closestSqrDist = sqrDist;
closest = enemy.transform;
}
}
return closest;
}
// 🔄 무기 휘두르는 순간 가챠 매니저의 허용 int 수치만큼 태그 배열을 순차 타격하는 광역 연산 함수
private void ExecuteMultiAttackPhysics()
{
if (mainTargetEnemy == null || IsTargetDead(mainTargetEnemy)) return;
// 아직 가챠를 안 뽑았거나(-1) 세팅 오류 시 기본 방화벽 마릿수 3마리 세팅
int maxTargetsCount = 3;
if (_gachaManager != null)
{
if (_gachaManager.CurrentHighestWeaponIndex >= 0)
{
maxTargetsCount = _gachaManager.GetCurrentWeaponMaxTargets();
}
}
if (maxTargetsCount <= 1) maxTargetsCount = 3;
// 공격 루프 직전, 가챠에서 불러온 정직한 수치를 디버그 창에 대입합니다. (20으로 튀는 버그 박멸)
currentMaxTargetsDisplay = maxTargetsCount;
// 맵에 있는 전 태그 몬스터를 안전 배열로 일제히 서치
GameObject[] allEnemies = GameObject.FindGameObjectsWithTag(EnemyTag);
List<System.Tuple<float, MonsterHP>> validTargets = new List<System.Tuple<float, MonsterHP>>();
var myPos = (Vector2)transform.position;
foreach (var enemyObj in allEnemies)
{
if (enemyObj == null || enemyObj.activeInHierarchy == false || enemyObj == gameObject) continue;
var monsterHP = enemyObj.GetComponent<MonsterHP>();
if (monsterHP == null || monsterHP.IsDead) continue;
float distance = Vector2.Distance(myPos, enemyObj.transform.position);
if (distance <= multiAttackRadius)
{
validTargets.Add(new System.Tuple<float, MonsterHP>(distance, monsterHP));
}
}
validTargets.Sort((a, b) => a.Item1.CompareTo(b.Item1));
int attackCount = 0;
// 🌟 가챠 매니저로부터 보너스 대미지를 실시간으로 가산 받아 최종 대미지 완성!
int baseDamage = PlayerStats.Instance != null ? PlayerStats.Instance.atkDamage : 10;
int bonusDamage = _gachaManager != null ? _gachaManager.GetCurrentWeaponBonusDamage() : 0;
int finalDamage = baseDamage + bonusDamage;
for (int i = 0; i < validTargets.Count; i++)
{
// 리스트 파괴 버그 없이, 순수 정수 카운트가 내 무기 한도에 도달하면 루프를 즉시 탈출!
if (attackCount >= maxTargetsCount) break;
if (validTargets[i].Item2 != null)
{
validTargets[i].Item2.TakeDamage(finalDamage);
attackCount++;
}
}
}
private void MoveTowardEnemy(Vector2 direction, float directionLength)
{
if (directionLength <= Mathf.Epsilon) return;
// 🌟 가챠 무기의 보너스 이동 속도를 실시간 가산 적용
float bonusMoveSpeed = _gachaManager != null ? _gachaManager.GetCurrentWeaponBonusMoveSpeed() : 0f;
float finalMoveSpeed = moveSpeed + bonusMoveSpeed;
var move = direction / directionLength * (finalMoveSpeed * Time.deltaTime);
var next = (Vector2)transform.position + move;
transform.position = new Vector3(next.x, next.y, transform.position.z);
}
private StartAttackCoroutine() { ... }
private StopAttackCoroutine() { ... }
private IEnumerator AttackLoop()
{
while (true)
{
// 🌟 가챠 무기의 보너스 공격 속도를 애니메이션 속도 제어기 배율에 실시간 결합
float bonusAtkSpeed = _gachaManager != null ? _gachaManager.GetCurrentWeaponBonusAttackSpeed() : 0f;
float speedMultiplier = 1f + bonusAtkSpeed;
float scaledDuration = attackDuration / speedMultiplier;
float scaledInterval = attackInterval / speedMultiplier;
isAttacking = true;
if (_animator != null) _animator.speed = speedMultiplier;
_animator.SetTrigger(AttackHash);
ExecuteMultiAttackPhysics();
yield return new WaitForSeconds(scaledDuration);
isAttacking = false;
if (_animator != null) _animator.speed = 1f;
float restTime = Mathf.Max(scaledInterval - scaledDuration, 0f);
yield return new WaitForSeconds(restTime);
}
}
}
🛠️ 3) 3단계: 유니티 에디터에서 마우스로 '무기 프리팹 데이터'와 '장착 손 위치' 최종 바인딩하기
뇌가 유기적으로 완벽해졌으니 유니티 엔진으로 돌아와 뽑힌 무기가 어느 위치에 생성되어 영웅의 손에 달라붙어야 하는지 마우스 손선으로 명확히 짚어주어야 합니다.
- 유니티 좌측 [Hierarchy(하이어라키)] 창에서 가챠 시스템을 총괄하는 GachaManager 오브젝트를 마우스 왼쪽 버튼으로 클릭합니다.
- 우측 [Inspector(인스펙터)] 창을 내려다보면 GachaManager 컴포넌트 아래에 새롭게 추가된 Spawn Point 칸과 Equip Parent Point 빈 칸(None) 2개가 보입니다.
- [뽑기 연출 스폰 위치 연결] : [Hierarchy] 창에서 무기가 하늘에서 팅겨 나와 떨어질 임시 위치 오브젝트인 SpawnPoint를 마우스로 꾹 잡고 우측 인스펙터의 Spawn Point 칸에 드래그 앤 드롭으로 골인시킵니다.
- [영웅의 실시간 장착 손 위치 연결] : 이번에는 [Hierarchy] 창에서 플레이어 캐릭터의 뼈대 자식 구조를 타고 들어가, 무기가 자석처럼 달라붙을 실제 영웅의 손 오브젝트인 EquipPoint를 꾹 잡고 우측 인스펙터의 Equip Parent Point 칸에 드래그 앤 드롭으로 쏙 넣어 줍니다!

🎨 4) 4단계: 디테일 연출 - 무기 등급별 광역 공격 마릿수($\text{maxTargets}$) 밸런싱 세팅하기
우리가 장비 뽑기를 돌렸을 때 등급 높은 무기일수록 더 많은 도마뱀 몬스터를 동시에 쓸어버리는 도파민 스펙을 기획에 맞춰 세팅해 주어야 합니다. 장착 보너스를 확실하게 커스텀해 보겠습니다.
- 프로젝트 창에서 무기 데이터 정보가 들어있는 가챠 매니저 혹은 인스펙터의 Weapon Data List 항목을 찾습니다.
- 리스트의 슬롯들을 하나씩 확장해보면 우리가 세팅해둔 등급별 무기(Element 0: 일반 단검, Element 1: 희귀 롱소드, Element 2: 전설 대검 등)가 배치되어 있습니다.
- 중간 항목에 있는 Max Targets (광역 공격 설정) 칸을 마우스로 딸깍 클릭합니다.
- 일반 무기는 수치를 3으로, 희귀 등급 무기는 4로, 대망의 전설 등급 무기는 사방의 적들을 한 번에 쓸어버릴 수 있도록 5 이상의 정수로 넉넉하게 지정한 뒤 저장(Ctrl + S)해 줍니다.

🕹️ 5) 5단계: 최종 조립 검증 및 인게임 대도파민 테스트!
모든 시스템이 완벽하게 결합되었습니다! 유니티 상단 중앙의 [▶ 플레이] 버튼을 누르고 뽑기 업그레이드 버튼을 지시등 보듯 연타해 보세요. 😎
- [외형 및 스탯 실시간 변환 검증] : 가챠 버튼을 눌러 새로운 고등급 무기가 뽑히는 그 정확한 1프레임의 순간! 영웅의 손에 들려있던 낡은 칼 도트 프리팹이 번쩍이며 즉시 파괴되고, 방금 뽑은 거대하고 화려한 전설 대검 오브젝트가 자석처럼 영웅의 손에 꽉 쥐어지며 인게임 Sprite 외형이 실시간 교체되는 것을 확인합니다.
- [디버그 창의 정직한 연동 수치] : 그와 동시에 Player 오브젝트의 인스펙터를 관찰하면, 주변에 도마뱀 몬스터가 20마리가 쌓이든 30마리가 쌓이든 상관없이 Current Max Targets Display 수치가 엉뚱하게 튀지 않고, 내가 방금 뽑아 장착한 무기 고유 스펙 마릿수인 3 혹은 4로 정직하고 단단하게 고정 표기되는 쾌감을 맛봅니다.
- [1 고정 버그 파괴 및 핵앤슬래시 실현] : 영웅이 몬스터 무리 한가운데로 바짝 진입해 칼을 휘두르는 찰나! 인스펙터 리스트가 1로 쪼그라들던 예전의 지독한 버그가 완전히 사라지고, 내 원형 사정거리 레이더망 안에 들어온 도마뱀 몬스터 4~5마리가 단 한 번의 칼질에 동시에 피격 팝업 대미지를 파파팍! 뿜어내며 낙엽처럼 쓸려나간다면 완벽하게 대성공입니다! 🎉

📖 6) 결론 및 다음 강의 예고
오늘 우리는 10강 강좌를 통해 장비를 뽑았을 때 외형 비주얼과 실제 인게임 전투 스탯 보너스를 0프레임 오차 없이 완벽하게 직통 동기화하는 영웅 장착 아키텍처를 마스터했습니다. 이제 비주얼과 수치가 완벽하게 톱니바퀴처럼 맞물리는 웰메이드 핵앤슬래시의 근간이 완성되었습니다. 😎
[제11강] 예고 🗓️
돈을 모으고 장비를 뽑아 영웅을 영구적으로 폭발 성장시키는 모든 클린 인프라를 구축했으니, 이제 이 영웅의 스펙 성장에 맞춰 사냥터도 함께 진화해야겠죠? 다음 시간에는 스테이지가 올라갈수록 몬스터들의 피통과 공격력이 수학적 밸런싱 공식에 맞춰 동적으로 스케일링되고, 획득 재화 보상 배율도 함께 인플레이션되는 "단계별 몬스터 동적 성장 스케일링 및 스테이지 재화 밸런싱 아키텍처 공식"을 탑재해 보겠습니다. 의식의 흐름 없이 정석대로 11강에서 뵙겠습니다. 기대해 주세요!
'AI > Cursor - 게임 만들기' 카테고리의 다른 글
| [제9강] 방치형 게임의 꽃: 확률 테이블 기반의 픽셀 장비 뽑기(Gacha) 메커니즘 빌드 (0) | 2026.06.10 |
|---|---|
| [제8강] UI 고도화: 버튼 텍스트 실시간 가격·스탯 반영 및 지갑 사정(Interactable) 불빛 연출 (0) | 2026.06.06 |
| [제7강] 방치형 RPG의 무한 성장 루프: 골드 소모 스탯 업그레이드 UI 버튼 및 성장 시스템 구현 (0) | 2026.06.02 |
| [제6강] 유니티 UI 캔버스 기초와 TextMeshPro 완벽 조작 및 대미지 팝업 연출 세팅 (0) | 2026.05.28 |
| [제5강] 몬스터 HP 시스템 구현 및 자석처럼 흡수되는 방치형 골드 드롭 시스템 제작 (0) | 2026.05.26 |