Files
Project-AI-Chronicle/unity-client/Assets/Scripts/Player.cs
T

353 lines
12 KiB
C#

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<Rigidbody2D>();
// 2. 부모 오브젝트에 붙어 있는 SPUM_Prefabs를 가져옵니다.
spumPrefab = GetComponentInParent<SPUM_Prefabs>();
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<Animator>();
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;
}
// 배틀 스테이지 씬에서는 이동 입력을 무시하여 움직이지 못하게 차단합니다 (채팅 공격 전용)
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "battleStage")
{
_inputs.PlayerMove = Vector2.zero;
}
else
{
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 || UnityEngine.SceneManagement.SceneManager.GetActiveScene().name == "battleStage")
{
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)
{
// 1. 플레이어 전사 사운드 재생
if (SoundManager.Instance != null)
{
SoundManager.Instance.PlayPlayerDeath();
}
animator.SetTrigger("4_Death");
Debug.Log($"플레이어가 {attackerName}에 의해 사망했습니다.");
StartCoroutine(SendDeathReport(attackerName));
}
else
{
// 2. 플레이어 피격 사운드 재생
if (SoundManager.Instance != null)
{
SoundManager.Instance.PlayPunchHit();
}
animator.SetTrigger("3_Damaged");
}
}
}
// 백엔드로부터 다음 세대 번호를 받아와 랜덤 이름과 매핑하는 코루틴
private System.Collections.IEnumerator GetNextGenerationFromServer()
{
string url = "http://192.168.1.7: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<NextGenerationResponse>(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://192.168.1.7: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);
}
// 1. 싱글톤 GameManager의 이전 캐릭터 정보 데이터 초기화 (신규 세대 생성 유도)
if (GameManager.Instance != null)
{
GameManager.Instance.ClearPlayerData();
}
// 2. 플레이어 사망 연출(2초 누워있기) 대기 후 로비 씬으로 씬 전환 실행
yield return new WaitForSeconds(2.0f);
Debug.Log("[Death Report] 로비 씬(Lobby)으로 되돌아갑니다.");
// 사망 연출이 종료되었으므로 플레이어 게임 오브젝트를 씬에서 완전히 파괴합니다.
Destroy(gameObject);
UnityEngine.SceneManagement.SceneManager.LoadScene("Lobby");
}
}
}
// 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;
}