using UnityEngine; using UnityEngine.UI; using TMPro; public struct Inputs { public Vector2 PlayerMove; } public class InputContoller { public static void UpdateInput(ref Inputs input) { input.PlayerMove.x = Input.GetAxisRaw("Horizontal"); input.PlayerMove.y = Input.GetAxisRaw("Vertical"); } } public class Player : MonoBehaviour { // 컴포넌트 프로퍼티 public Rigidbody2D rb { get; private set; } // SPUM_Prefabs를 가져옵니다. private SPUM_Prefabs spumPrefab; private float maxSpeed; private Inputs _inputs; public Inputs Inputse => _inputs; // 이동 관련 변수 [SerializeField] private float speed = 5f; [SerializeField] public int hp = 5; [SerializeField] public int maxHp = 5; private bool isFacingRight = true; private int currentAnimationState = -1; private Animator animator; // 캐릭터 생성 정보 캐싱 private string characterName = "데이터 로딩 중..."; private int generation = 1; public int Generation => generation; public string CharacterName => characterName; [Header("UI 연결")] public TextMeshProUGUI nameText; public Image hpImage; private static readonly string[] RANDOM_NAMES = new string[] { "자르반", "가렌", "럭스", "다리우스", "야스오", "티모", "아리", "이즈리얼", "조이", "제이스", "카이사", "제드", "탈론", "리븐", "애쉬", "트린다미어", "소나", "룰루", "유미", "레오나", "다이애나", "세주아니", "우디르", "신 짜오" }; void Awake() { // 1. Rigidbody2D 할당 rb = GetComponent(); // 2. 부모 오브젝트에 붙어 있는 SPUM_Prefabs를 가져옵니다. spumPrefab = GetComponentInParent(); if (spumPrefab == null) { Debug.LogError($"{gameObject.name} 오브젝트에 SPUM_Prefabs 컴포넌트가 없습니다! 인스펙터를 확인하세요."); } // 오리지널 리소스가 원래 왼쪽을 보고 있으므로, 시작 시 X 스케일을 반전시켜 즉시 오른쪽을 바라보도록 초기화합니다. Vector3 initialScale = transform.localScale; initialScale.x = -Mathf.Abs(initialScale.x); transform.localScale = initialScale; animator = GetComponentInChildren(); maxHp = hp; } void Start() { // 씬 로딩 후 GameManager에 이미 보존되어 넘어온 캐릭터 정보가 존재한다면 복구합니다. if (GameManager.Instance != null && !string.IsNullOrEmpty(GameManager.Instance.playerName)) { characterName = GameManager.Instance.playerName; generation = GameManager.Instance.playerGeneration; hp = GameManager.Instance.playerHp; maxHp = GameManager.Instance.playerMaxHp; // UI 텍스트 및 이미지 게이지 동기화 if (nameText != null) { nameText.text = characterName; } if (hpImage != null && maxHp > 0) { float fillRatio = Mathf.Clamp01((float)hp / maxHp); hpImage.fillAmount = fillRatio; hpImage.rectTransform.localScale = new Vector3(fillRatio, 1f, 1f); } Debug.Log($"[Player] GameManager로부터 이전 캐릭터 정보를 무사히 인계받았습니다: {characterName} | HP: {hp}/{maxHp}"); } else { // 데이터가 보존되지 않은 최초 기동 상태인 경우에만 백엔드 서버에서 신규 세대 번호를 조회하여 할당합니다. StartCoroutine(GetNextGenerationFromServer()); } } private void Update() { // 안전 조치: maxHp가 비정상(0 이하)이거나 현재 체력이 초과된 경우 자동 동기화 if (maxHp <= 0 || hp > maxHp) { maxHp = hp; } if (hpImage != null && maxHp > 0) { float fillRatio = Mathf.Clamp01((float)hp / maxHp); hpImage.fillAmount = fillRatio; // 이미지 컴포넌트의 타입(Simple, Filled 등)과 상관없이 무조건 가로 크기가 깎이도록 // RectTransform의 로컬 스케일 X축도 비율에 맞춰 실시간으로 동기화합니다. hpImage.rectTransform.localScale = new Vector3(fillRatio, 1f, 1f); } if (hp <= 0) { if (animator != null) { animator.SetBool("1_Move", false); } return; } InputContoller.UpdateInput(ref _inputs); // 좌우 방향 전환 (Flip) if (_inputs.PlayerMove.x < 0f && isFacingRight) { Flip(); } else if (_inputs.PlayerMove.x > 0f && !isFacingRight) { Flip(); } // 애니메이션 업데이트 호출 if (_inputs.PlayerMove.x != 0f || _inputs.PlayerMove.y != 0f) { animator.SetBool("1_Move", true); } else { animator.SetBool("1_Move", false); } if (Input.GetKeyDown(KeyCode.Z)) { animator.SetTrigger("2_Attack"); } } void FixedUpdate() { Move(); } private void Move() { if (rb == null) return; if (hp <= 0) { rb.linearVelocity = new Vector2(0f, rb.linearVelocity.y); return; } rb.linearVelocity = new Vector2(_inputs.PlayerMove.x * speed, rb.linearVelocity.y); } private void Flip() { isFacingRight = !isFacingRight; Vector3 scale = transform.localScale; scale.x *= -1f; transform.localScale = scale; } // 데미지를 입는 메서드 public void TakeDamage(int damage, string attackerName = "알 수 없는 위험") { if (hp <= 0) return; // 이미 사망한 경우 제외 hp -= damage; if (hp < 0) hp = 0; Debug.Log($"플레이어가 {attackerName}로부터 {damage} 데미지를 입었습니다. 현재 체력: {hp}"); if (animator != null) { if (hp <= 0) { animator.SetTrigger("4_Death"); Debug.Log($"플레이어가 {attackerName}에 의해 사망했습니다."); StartCoroutine(SendDeathReport(attackerName)); } else { animator.SetTrigger("3_Damaged"); } } } // 인스펙터 우클릭 메뉴에서 데미지를 테스트할 수 있는 기능 추가 [ContextMenu("Debug/Take 5 Damage")] public void DebugTake5Damage() { TakeDamage(5, "인스펙터 디버그 공격"); } [ContextMenu("Debug/Restore HP")] public void DebugRestoreHP() { hp = maxHp; if (animator != null) { animator.Rebind(); animator.Update(0f); } Debug.Log($"플레이어 체력이 {hp}로 회복되었습니다."); // 체력 복구 시 다음 캐릭터 이름 및 세대 정보 갱신 StartCoroutine(GetNextGenerationFromServer()); } // 임시 디버그용 화면 버튼 (게임 실행 시 화면 좌측 상단에 뜸) private void OnGUI() { GUI.backgroundColor = Color.red; if (GUI.Button(new Rect(10, 10, 180, 50), "Debug: Take 5 Damage (Trap)")) { TakeDamage(5, "테스트 함정"); } GUI.backgroundColor = Color.blue; // 기존 겹쳐있던 위치(10, 10)를 피해 y 좌표를 130으로 내렸습니다. if (GUI.Button(new Rect(10, 130, 180, 50), "Debug: Take 3 Damage (Monster)")) { TakeDamage(3, "테스트 몬스터"); } GUI.backgroundColor = Color.green; if (GUI.Button(new Rect(10, 70, 180, 50), "Debug: Restore HP")) { DebugRestoreHP(); } // 캐릭터 정보 UI 노출 GUI.color = Color.white; GUI.skin.label.fontSize = 16; GUI.Label(new Rect(10, 200, 400, 30), $"이름: {characterName}"); GUI.Label(new Rect(10, 230, 400, 30), $"세대: {generation}대손"); } // 백엔드로부터 다음 세대 번호를 받아와 랜덤 이름과 매핑하는 코루틴 private System.Collections.IEnumerator GetNextGenerationFromServer() { string url = "http://127.0.0.1:8000/api/game/generation/next"; using (UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.Get(url)) { yield return request.SendWebRequest(); if (request.result == UnityEngine.Networking.UnityWebRequest.Result.ConnectionError || request.result == UnityEngine.Networking.UnityWebRequest.Result.ProtocolError) { Debug.LogError("다음 세대 번호 조회 실패: " + request.error); // 실패 시 로컬 기본값 적용 generation = 1; } else { try { string json = request.downloadHandler.text; NextGenerationResponse response = JsonUtility.FromJson(json); generation = response.next_generation; Debug.Log($"서버로부터 다음 세대 번호({generation}대) 조회 성공."); } catch (System.Exception ex) { Debug.LogError("세대 JSON 파싱 에러: " + ex.Message); generation = 1; } } } // 이름 랜덤 선택 후 세대 접미사 추가 (예: 자르반 3세) string baseName = RANDOM_NAMES[Random.Range(0, RANDOM_NAMES.Length)]; characterName = $"{baseName} {generation}세"; if (nameText != null) { nameText.text = characterName; } Debug.Log($"캐릭터 생성 완료! 이름: {characterName}, 세대: {generation}대"); } // 백엔드로 완성된 사망 기록을 전송하는 코루틴 private System.Collections.IEnumerator SendDeathReport(string cause) { string url = "http://127.0.0.1:8000/api/game/death"; AncestorDeathCreate requestData = new AncestorDeathCreate { name = characterName, generation = generation, cause_of_death = cause, floor = 1 // 장소는 임시로 1층 고정 }; string json = JsonUtility.ToJson(requestData); using (UnityEngine.Networking.UnityWebRequest request = new UnityEngine.Networking.UnityWebRequest(url, "POST")) { byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json); request.uploadHandler = new UnityEngine.Networking.UploadHandlerRaw(bodyRaw); request.downloadHandler = new UnityEngine.Networking.DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); yield return request.SendWebRequest(); if (request.result == UnityEngine.Networking.UnityWebRequest.Result.ConnectionError || request.result == UnityEngine.Networking.UnityWebRequest.Result.ProtocolError) { Debug.LogError("사망 기록 전송 실패: " + request.error); } else { Debug.Log("사망 기록 전송 성공: " + request.downloadHandler.text); // 사망 전송 성공 즉시 다음 세대 정보 받아와 자동 갱신 StartCoroutine(GetNextGenerationFromServer()); } } } } // JSON 파싱용 세대 응답 스키마 객체 [System.Serializable] public class NextGenerationResponse { public int next_generation; } // JSON 전송용 사망 기록 객체 [System.Serializable] public class AncestorDeathCreate { public string name; public int generation; public string cause_of_death; public int floor; }