캐릭터 움직임 및 애니메이션

This commit is contained in:
2026-03-09 15:31:30 +09:00
parent 1e8c23e128
commit 2aa52746e9
7613 changed files with 11324200 additions and 1724 deletions

View File

@@ -0,0 +1,19 @@
{
"name": "Colosseum.Game",
"rootNamespace": "Colosseum",
"references": [
"Unity.Netcode.Runtime",
"Unity.Networking.Transport",
"Unity.Transport",
"Unity.InputSystem"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 597e1695ce4bb584f96f8673b0cf7a6a
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d3cd76473ef5dcf44afccfab5fbdbfc6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace Colosseum.UI.Editor
{
[CustomEditor(typeof(ConnectionUI))]
public class ConnectionUIEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("Connection Controls", EditorStyles.boldLabel);
ConnectionUI connectionUI = (ConnectionUI)target;
EditorGUI.BeginDisabledGroup(!Application.isPlaying);
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Start Host", GUILayout.Height(30)))
{
connectionUI.StartHost();
}
if (GUILayout.Button("Start Client", GUILayout.Height(30)))
{
connectionUI.StartClient();
}
if (GUILayout.Button("Start Server", GUILayout.Height(30)))
{
connectionUI.StartServer();
}
}
if (GUILayout.Button("Disconnect", GUILayout.Height(25)))
{
connectionUI.Disconnect();
}
EditorGUI.EndDisabledGroup();
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("Play Mode에서만 연결할 수 있습니다.", MessageType.Info);
}
}
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 088a15576b464764b85a18f4dacb1a43

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5b884d077dc9f2543a6b5cca0957e6d6

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 052faaac586de48259a63d0c4782560b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 1
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee7b4209244032546a087316c8f2f2ed
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,45 @@
using UnityEngine;
using Unity.Netcode;
namespace Colosseum.Network
{
/// <summary>
/// 네트워크 연결 관리 (Host/Client 시작)
/// </summary>
public class NetworkLauncher : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private GameObject connectionUI;
private void Start()
{
// 게임 시작 시 UI 표시
if (connectionUI != null)
connectionUI.SetActive(true);
}
public void StartHost()
{
NetworkManager.Singleton.StartHost();
HideConnectionUI();
}
public void StartClient()
{
NetworkManager.Singleton.StartClient();
HideConnectionUI();
}
public void StartServer()
{
NetworkManager.Singleton.StartServer();
HideConnectionUI();
}
private void HideConnectionUI()
{
if (connectionUI != null)
connectionUI.SetActive(false);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a94a6fa23cedaf545aea2b1057800149

View File

@@ -0,0 +1,56 @@
using UnityEngine;
namespace Colosseum.Network
{
/// <summary>
/// 플레이어 스폰 위치 마커
/// 씬에 배치하여 플레이어가 스폰될 위치를 지정
/// </summary>
public class PlayerSpawnPoint : MonoBehaviour
{
[Header("Spawn Settings")]
[SerializeField] private bool useRotation = true;
private static System.Collections.Generic.List<PlayerSpawnPoint> spawnPoints = new System.Collections.Generic.List<PlayerSpawnPoint>();
private void Awake()
{
spawnPoints.Add(this);
}
private void OnDestroy()
{
spawnPoints.Remove(this);
}
/// <summary>
/// 사용 가능한 스폰 포인트 중 하나를 반환
/// </summary>
public static Transform GetRandomSpawnPoint()
{
if (spawnPoints.Count == 0)
return null;
int index = Random.Range(0, spawnPoints.Count);
return spawnPoints[index].transform;
}
/// <summary>
/// 스폰 포인트 개수 반환
/// </summary>
public static int SpawnPointCount => spawnPoints.Count;
private void OnDrawGizmos()
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(transform.position, 0.5f);
Gizmos.color = Color.blue;
Gizmos.DrawLine(transform.position, transform.position + transform.forward * 1.5f);
// 화살표 머리
Vector3 arrowPos = transform.position + transform.forward * 1.5f;
Gizmos.DrawLine(arrowPos, arrowPos - transform.forward * 0.3f + transform.right * 0.2f);
Gizmos.DrawLine(arrowPos, arrowPos - transform.forward * 0.3f - transform.right * 0.2f);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: aba9a2eb2571da14d901ed51c8866f47

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9b58faa7a971b2c4aa9cadf4132ac7a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,98 @@
using UnityEngine;
using Unity.Netcode;
namespace Colosseum.Player
{
/// <summary>
/// 플레이어 애니메이션 컨트롤러
/// 이동 속도에 따라 Idle/Walk/Run 애니메이션 제어
/// </summary>
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(PlayerMovement))]
public class PlayerAnimationController : NetworkBehaviour
{
[Header("Animation Parameters")]
[SerializeField] private string speedParam = "Speed";
[SerializeField] private string isGroundedParam = "IsGrounded";
[SerializeField] private string jumpTriggerParam = "Jump";
[SerializeField] private string landTriggerParam = "Land";
[Header("Settings")]
[SerializeField] private float speedSmoothTime = 0.1f;
private Animator animator;
private PlayerMovement playerMovement;
private CharacterController characterController;
private float currentSpeed;
private float speedVelocity;
private bool wasGrounded = true;
private bool isJumpingAnimation;
private void Awake()
{
animator = GetComponent<Animator>();
playerMovement = GetComponent<PlayerMovement>();
characterController = GetComponent<CharacterController>();
}
public override void OnNetworkSpawn()
{
if (!IsOwner)
{
enabled = false;
}
}
private void Update()
{
if (!IsOwner) return;
UpdateAnimationParameters();
}
private void UpdateAnimationParameters()
{
// PlayerMovement에서 직접 속도 가져오기
float targetSpeed = playerMovement.CurrentMoveSpeed;
// 부드러운 속도 변화
currentSpeed = Mathf.SmoothDamp(currentSpeed, targetSpeed, ref speedVelocity, speedSmoothTime);
// 지면 접촉 상태
bool isGrounded = characterController.isGrounded;
// 착지 감지 (공중에서 지면으로)
if (!wasGrounded && isGrounded && isJumpingAnimation)
{
PlayLand();
}
// 애니메이터 파라미터 설정
animator.SetFloat(speedParam, currentSpeed);
animator.SetBool(isGroundedParam, isGrounded);
wasGrounded = isGrounded;
}
/// <summary>
/// 점프 애니메이션 트리거 (외부에서 호출)
/// </summary>
public void PlayJump()
{
if (IsOwner)
{
isJumpingAnimation = true;
animator.SetTrigger(jumpTriggerParam);
}
}
/// <summary>
/// 착지 애니메이션 트리거
/// </summary>
private void PlayLand()
{
isJumpingAnimation = false;
animator.SetTrigger(landTriggerParam);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dafca28b13e62ee43893a43187dc3535

View File

@@ -0,0 +1,76 @@
using UnityEngine;
using UnityEngine.InputSystem;
namespace Colosseum.Player
{
/// <summary>
/// 3인칭 카메라 컨트롤러
/// </summary>
public class PlayerCamera : MonoBehaviour
{
[Header("Camera Settings")]
[SerializeField] private float distance = 5f;
[SerializeField] private float height = 2f;
[SerializeField] private float rotationSpeed = 2f;
[SerializeField] private float minPitch = -30f;
[SerializeField] private float maxPitch = 60f;
private Transform target;
private float yaw;
private float pitch;
private Camera cameraInstance;
private InputSystem_Actions inputActions;
public void Initialize(Transform playerTransform, InputSystem_Actions actions)
{
target = playerTransform;
inputActions = actions;
// 기존 메인 카메라 사용 또는 새로 생성
cameraInstance = Camera.main;
if (cameraInstance == null)
{
var cameraObject = new GameObject("PlayerCamera");
cameraInstance = cameraObject.AddComponent<Camera>();
cameraObject.tag = "MainCamera";
}
// 초기 각도
yaw = target.eulerAngles.y;
pitch = 20f;
}
private void LateUpdate()
{
if (target == null || cameraInstance == null) return;
HandleRotation();
UpdateCameraPosition();
}
private void HandleRotation()
{
if (inputActions == null) return;
// Input Actions에서 Look 입력 받기
Vector2 lookInput = inputActions.Player.Look.ReadValue<Vector2>();
float mouseX = lookInput.x * rotationSpeed * 0.1f;
float mouseY = lookInput.y * rotationSpeed * 0.1f;
yaw += mouseX;
pitch -= mouseY;
pitch = Mathf.Clamp(pitch, minPitch, maxPitch);
}
private void UpdateCameraPosition()
{
// 구면 좌표로 카메라 위치 계산
Quaternion rotation = Quaternion.Euler(pitch, yaw, 0f);
Vector3 offset = rotation * new Vector3(0f, 0f, -distance);
offset.y += height;
cameraInstance.transform.position = target.position + offset;
cameraInstance.transform.LookAt(target.position + Vector3.up * height * 0.5f);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3ffd5f6c47d39e94f92515c69e69a9a1

View File

@@ -0,0 +1,234 @@
using UnityEngine;
using UnityEngine.InputSystem;
using Unity.Netcode;
using Colosseum.Network;
namespace Colosseum.Player
{
/// <summary>
/// 3인칭 플레이어 이동 (네트워크 동기화)
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement : NetworkBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float rotationSpeed = 10f;
[SerializeField] private float gravity = -9.81f;
[Header("Jump Settings")]
[SerializeField] private float jumpForce = 5f;
private CharacterController characterController;
private Vector3 velocity;
private Vector2 moveInput;
private InputSystem_Actions inputActions;
private bool isJumping;
private bool wasGrounded;
/// <summary>
/// 현재 이동 속도 (애니메이션용)
/// </summary>
public float CurrentMoveSpeed => moveInput.magnitude * moveSpeed;
/// <summary>
/// 현재 지면 접촉 상태
/// </summary>
public bool IsGrounded => characterController != null ? characterController.isGrounded : false;
/// <summary>
/// 점프 중 상태
/// </summary>
public bool IsJumping => isJumping;
public override void OnNetworkSpawn()
{
// 로컬 플레이어가 아니면 입력 비활성화
if (!IsOwner)
{
enabled = false;
return;
}
characterController = GetComponent<CharacterController>();
// 스폰 포인트에서 위치 설정
SetSpawnPosition();
// Input Actions 초기화
inputActions = new InputSystem_Actions();
inputActions.Player.Enable();
// Move 액션 콜백 등록
inputActions.Player.Move.performed += OnMovePerformed;
inputActions.Player.Move.canceled += OnMoveCanceled;
// Jump 액션 콜백 등록
inputActions.Player.Jump.performed += OnJumpPerformed;
// 카메라 설정
SetupCamera();
}
/// <summary>
/// 스폰 위치 설정
/// </summary>
private void SetSpawnPosition()
{
Transform spawnPoint = PlayerSpawnPoint.GetRandomSpawnPoint();
if (spawnPoint != null)
{
// CharacterController 비활성화 후 위치 설정 (충돌 문제 방지)
characterController.enabled = false;
transform.position = spawnPoint.position;
transform.rotation = spawnPoint.rotation;
characterController.enabled = true;
}
}
/// <summary>
/// 네트워크 정리리
/// </summary>
public override void OnNetworkDespawn()
{
if (inputActions != null)
{
inputActions.Player.Move.performed -= OnMovePerformed;
inputActions.Player.Move.canceled -= OnMoveCanceled;
inputActions.Player.Jump.performed -= OnJumpPerformed;
inputActions.Disable();
}
}
private void OnMovePerformed(InputAction.CallbackContext context)
{
moveInput = context.ReadValue<Vector2>();
}
private void OnMoveCanceled(InputAction.CallbackContext context)
{
moveInput = Vector2.zero;
}
private void OnJumpPerformed(InputAction.CallbackContext context)
{
if (!isJumping && characterController.isGrounded)
{
Jump();
}
}
private void SetupCamera()
{
var cameraController = GetComponent<PlayerCamera>();
if (cameraController == null)
{
cameraController = gameObject.AddComponent<PlayerCamera>();
}
cameraController.Initialize(transform, inputActions);
}
private void Update()
{
if (!IsOwner) return;
ApplyGravity();
Move();
}
private void ApplyGravity()
{
if (wasGrounded && velocity.y < 0)
{
velocity.y = -2f;
}
else
{
velocity.y += gravity * Time.deltaTime;
}
}
private void Move()
{
if (characterController == null) return;
// 이동 방향 계산 (카메라 기준)
Vector3 moveDirection = new Vector3(moveInput.x, 0f, moveInput.y);
moveDirection = TransformDirectionByCamera(moveDirection);
moveDirection.Normalize();
// 이동 적용
Vector3 moveVector = moveDirection * moveSpeed * Time.deltaTime;
characterController.Move(moveVector + velocity * Time.deltaTime);
// 회전 (이동 중일 때만)
if (moveDirection != Vector3.zero)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
}
// 착지 체크 (Move 후에 isGrounded가 업데이트됨)
if (!wasGrounded && characterController.isGrounded && isJumping)
{
OnJumpEnd();
}
// 다음 프레임을 위해 현재 상태 저장
wasGrounded = characterController.isGrounded;
}
private void Jump()
{
isJumping = true;
velocity.y = jumpForce;
// 애니메이션 컨트롤러에 점프 알림
var animController = GetComponent<PlayerAnimationController>();
if (animController != null)
{
animController.PlayJump();
}
}
/// <summary>
/// 점프 중 상태가 끝나면 IsJumping = false;
/// </summary>
public void OnJumpEnd()
{
isJumping = false;
}
private Vector3 TransformDirectionByCamera(Vector3 direction)
{
if (Camera.main == null) return direction;
Transform cameraTransform = Camera.main.transform;
Vector3 cameraForward = cameraTransform.forward;
Vector3 cameraRight = cameraTransform.right;
// Y축 제거
cameraForward.y = 0f;
cameraRight.y = 0f;
cameraForward.Normalize();
cameraRight.Normalize();
return cameraRight * direction.x + cameraForward * direction.z;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: db1d9c2d6f86e254f9889e2fa9d41e31

View File

@@ -0,0 +1,72 @@
using UnityEngine;
using Unity.Netcode;
namespace Colosseum.Player
{
/// <summary>
/// 플레이어 네트워크 상태 관리 (HP, MP 등)
/// </summary>
public class PlayerNetworkController : NetworkBehaviour
{
[Header("Stats")]
[SerializeField] private float maxHealth = 100f;
[SerializeField] private float maxMana = 50f;
// 네트워크 동기화 변수
private NetworkVariable<float> currentHealth = new NetworkVariable<float>(100f);
private NetworkVariable<float> currentMana = new NetworkVariable<float>(50f);
public float Health => currentHealth.Value;
public float MaxHealth => maxHealth;
public float Mana => currentMana.Value;
public float MaxMana => maxMana;
public override void OnNetworkSpawn()
{
// 초기화
if (IsServer)
{
currentHealth.Value = maxHealth;
currentMana.Value = maxMana;
}
}
/// <summary>
/// 대미지 적용 (서버에서만 실행)
/// </summary>
[Rpc(SendTo.Server)]
public void TakeDamageRpc(float damage)
{
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - damage);
if (currentHealth.Value <= 0f)
{
HandleDeath();
}
}
/// <summary>
/// 마나 소모 (서버에서만 실행)
/// </summary>
[Rpc(SendTo.Server)]
public void UseManaRpc(float amount)
{
currentMana.Value = Mathf.Max(0f, currentMana.Value - amount);
}
/// <summary>
/// 마나 회복 (서버에서만 실행)
/// </summary>
[Rpc(SendTo.Server)]
public void RestoreManaRpc(float amount)
{
currentMana.Value = Mathf.Min(maxMana, currentMana.Value + amount);
}
private void HandleDeath()
{
// TODO: 사망 처리 로직
Debug.Log($"Player {OwnerClientId} died!");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f5b322d198fc60a41ae175ea9c58a337

8
Assets/Scripts/UI.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 99571f232b8b003448d6d5eba9562fb4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,100 @@
using UnityEngine;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
namespace Colosseum.UI
{
/// <summary>
/// 네트워크 연결 설정 (Inspector에서 제어)
/// </summary>
public class ConnectionUI : MonoBehaviour
{
[Header("Connection Settings")]
[SerializeField] private string ipAddress = "127.0.0.1";
[SerializeField] private ushort port = 7777;
[Header("Status (Read Only)")]
[SerializeField, Tooltip("현재 연결 상태")] private string connectionStatus = "Disconnected";
private UnityTransport transport;
private void Awake()
{
transport = NetworkManager.Singleton?.GetComponent<UnityTransport>();
}
private void Start()
{
UpdateTransportSettings();
}
private void Update()
{
UpdateConnectionStatus();
}
private void UpdateConnectionStatus()
{
if (NetworkManager.Singleton == null)
{
connectionStatus = "No NetworkManager";
return;
}
if (NetworkManager.Singleton.IsServer && NetworkManager.Singleton.IsHost)
connectionStatus = "Host";
else if (NetworkManager.Singleton.IsServer)
connectionStatus = "Server";
else if (NetworkManager.Singleton.IsClient)
connectionStatus = NetworkManager.Singleton.IsConnectedClient ? "Connected" : "Connecting...";
else
connectionStatus = "Disconnected";
}
public void StartHost()
{
UpdateTransportSettings();
NetworkManager.Singleton.StartHost();
Debug.Log("[Network] Started as Host");
}
public void StartClient()
{
UpdateTransportSettings();
NetworkManager.Singleton.StartClient();
Debug.Log($"[Network] Connecting to {ipAddress}:{port}...");
}
public void StartServer()
{
UpdateTransportSettings();
NetworkManager.Singleton.StartServer();
Debug.Log("[Network] Started as Server");
}
public void Disconnect()
{
if (NetworkManager.Singleton != null)
{
NetworkManager.Singleton.Shutdown();
Debug.Log("[Network] Disconnected");
}
}
private void UpdateTransportSettings()
{
if (transport != null)
{
transport.SetConnectionData(ipAddress, port);
}
}
private void OnValidate()
{
if (Application.isPlaying && transport != null)
{
UpdateTransportSettings();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 11a53a74e3d983f478f37ef0c99d5847