529 lines
19 KiB
C#
529 lines
19 KiB
C#
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using Unity.Netcode;
|
|
using System;
|
|
using Unity.Cinemachine;
|
|
using System.Collections;
|
|
|
|
[RequireComponent(typeof(NetworkObject))]
|
|
public class PlayerNetworkController : NetworkBehaviour
|
|
{
|
|
// ... 기존 변수들 유지 ...
|
|
[Header("Movement Settings")]
|
|
public float moveSpeed = 5f;
|
|
public float rotationSpeed = 10f;
|
|
public float jumpHeight = 1.5f;
|
|
public float gravity = -19.62f;
|
|
|
|
[Header("Interaction Settings")]
|
|
[SerializeField] private float interactRange = 3f;
|
|
[SerializeField] private LayerMask interactableLayer;
|
|
[SerializeField] private LayerMask constructionLayer;
|
|
[SerializeField] private float buildSpeedMultiplier = 2f;
|
|
[SerializeField] private LayerMask itemLayer; // DroppedItem 레이어 설정
|
|
|
|
[Header("Mining Settings")]
|
|
[SerializeField] private float attackRange = 3.5f;
|
|
[SerializeField] private float aimRadius = 0.5f;
|
|
[SerializeField] private int miningDamage = 50;
|
|
[SerializeField] private LayerMask mineableLayer;
|
|
[SerializeField] private LayerMask ignoreDuringAim; // 반드시 'Player' 레이어를 포함하세요!
|
|
|
|
[Header("Visual Feedback")]
|
|
[SerializeField] private float crosshairScreenRadius = 200f;
|
|
[SerializeField] private UnityEngine.UI.Image crosshairUI;
|
|
[SerializeField] private Sprite idleCrosshair;
|
|
[SerializeField] private Sprite targetCrosshair;
|
|
|
|
[Header("Fog of War Settings")]
|
|
[SerializeField] private float visionRadius = 5f; // 시야 반경
|
|
private float _lastRevealTime;
|
|
|
|
[Header("Inventory & Action")]
|
|
private PlayerInventory _inventory; // 인벤토리 참조 추가
|
|
private PlayerActionHandler _actionHandler;
|
|
|
|
private RectTransform _crosshairRect;
|
|
private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장
|
|
private MineableBlock _lastHighlightedBlock;
|
|
|
|
private CharacterController _controller;
|
|
private PlayerInputActions _inputActions;
|
|
private Animator _animator;
|
|
private TunnelTraveler _traveler;
|
|
|
|
private Vector2 _moveInput;
|
|
private Vector3 _velocity;
|
|
private Vector3 _currentMoveDir;
|
|
private bool _isGrounded;
|
|
private bool _isHoldingInteract = false;
|
|
|
|
private bool _isHoldingAction = false;
|
|
private bool _hasExecutedOnce = false; // 단발성 액션이 중복 실행되지 않도록 방지
|
|
|
|
// 현재 플레이어가 어떤 행동을 하고 있는지 나타내는 상태
|
|
public enum ActionState { Idle, Busy }
|
|
private ActionState _currentState = ActionState.Idle;
|
|
|
|
public bool IsBusy => _currentState == ActionState.Busy;
|
|
|
|
// 디버그 변수
|
|
private Vector3 _debugOrigin;
|
|
private Vector3 _debugDir;
|
|
private bool _debugHit;
|
|
private float _debugDist;
|
|
|
|
// NGO 초기화
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
if (!IsOwner) return;
|
|
|
|
// 1. 씬에 있는 가상 카메라를 찾습니다.
|
|
// Unity 6에서는 CinemachineVirtualCamera 대신 CinemachineCamera를 주로 사용합니다.
|
|
var vcam = GameObject.FindAnyObjectByType<CinemachineCamera>();
|
|
|
|
if (vcam != null)
|
|
{
|
|
// 2. 카메라의 Follow와 LookAt 대상을 '나'로 설정합니다.
|
|
vcam.Follow = transform;
|
|
vcam.LookAt = transform;
|
|
Debug.Log("<color=green>[Camera] 로컬 플레이어에게 카메라가 연결되었습니다.</color>");
|
|
}
|
|
|
|
// 씬의 Canvas 안에 있는 "Crosshair"라는 이름의 오브젝트를 찾습니다.
|
|
GameObject crosshairObj = GameObject.Find("Crosshair");
|
|
if (crosshairObj != null)
|
|
{
|
|
_crosshairRect = crosshairObj.GetComponent<RectTransform>();
|
|
crosshairUI = crosshairObj.GetComponent<UnityEngine.UI.Image>();
|
|
// 초기 스프라이트 설정
|
|
crosshairUI.sprite = idleCrosshair;
|
|
}
|
|
|
|
_inputActions = new PlayerInputActions();
|
|
_inputActions.Player.Jump.performed += ctx => OnJump();
|
|
_inputActions.Player.Action.performed += ctx => OnActionInput();
|
|
_inputActions.Player.Interact.performed += ctx => OnInteractTap(); // 탭 상호작용
|
|
|
|
// started: 버튼을 누르는 순간 즉시 첫 번째 시도
|
|
_inputActions.Player.Action.started += ctx => {
|
|
_isHoldingAction = true;
|
|
_hasExecutedOnce = false; // 누르기 시작할 때 초기화
|
|
TryExecuteAction();
|
|
};
|
|
|
|
// canceled: 버튼을 떼는 순간
|
|
_inputActions.Player.Action.canceled += ctx => {
|
|
_isHoldingAction = false;
|
|
};
|
|
|
|
_inputActions.Player.Select1.performed += ctx => _inventory.ChangeSelectedSlotRpc(0);
|
|
_inputActions.Player.Select2.performed += ctx => _inventory.ChangeSelectedSlotRpc(1);
|
|
_inputActions.Player.Select3.performed += ctx => _inventory.ChangeSelectedSlotRpc(2);
|
|
_inputActions.Player.Select4.performed += ctx => _inventory.ChangeSelectedSlotRpc(3);
|
|
_inputActions.Player.Select5.performed += ctx => _inventory.ChangeSelectedSlotRpc(4);
|
|
_inputActions.Player.Select6.performed += ctx => _inventory.ChangeSelectedSlotRpc(5);
|
|
|
|
_inputActions.Enable();
|
|
}
|
|
|
|
void Awake()
|
|
{
|
|
_controller = GetComponent<CharacterController>();
|
|
_animator = GetComponent<Animator>();
|
|
_traveler = GetComponent<TunnelTraveler>();
|
|
|
|
// --- 참조 초기화 추가 ---
|
|
_inventory = GetComponent<PlayerInventory>();
|
|
_actionHandler = GetComponent<PlayerActionHandler>();
|
|
|
|
if (_inventory == null) Debug.LogError("PlayerInventory 컴포넌트를 찾을 수 없습니다!");
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (!IsOwner) return;
|
|
if (_traveler != null && _traveler.IsTraveling) return;
|
|
|
|
HandleGravity();
|
|
HandleMovement();
|
|
|
|
if (_isHoldingInteract) PerformConstructionSupport();
|
|
|
|
UpdateCrosshairPosition(); // 이 안에서 움직여야 합니다.
|
|
UpdateTargetFeedback();
|
|
|
|
// 매 프레임 실행하면 무거우므로 0.2초마다 주변 시야 갱신
|
|
if (Time.time - _lastRevealTime > 0.2f)
|
|
{
|
|
_lastRevealTime = Time.time;
|
|
RevealSurroundings();
|
|
}
|
|
|
|
// 버튼을 꾹 누르고 있고, 아직 액션이 진행 중이 아닐 때만 반복 체크
|
|
if (_isHoldingAction && !_actionHandler.IsBusy)
|
|
{
|
|
HandleContinuousAction();
|
|
}
|
|
}
|
|
|
|
// --- 이동 관련 로직 (기존 유지) ---
|
|
private void HandleMovement()
|
|
{
|
|
_isGrounded = _controller.isGrounded;
|
|
_animator.SetBool("isGrounded", _isGrounded);
|
|
|
|
bool isAttacking = _animator.GetCurrentAnimatorStateInfo(0).IsTag("Attack");
|
|
_moveInput = _inputActions.Player.Move.ReadValue<Vector2>();
|
|
Vector3 move = new Vector3(_moveInput.x, 0, _moveInput.y).normalized;
|
|
|
|
if (isAttacking) move = _isGrounded ? Vector3.zero : _currentMoveDir;
|
|
else if (move.magnitude > 0.1f) _currentMoveDir = move;
|
|
|
|
if (move.magnitude >= 0.1f)
|
|
{
|
|
if (!isAttacking)
|
|
{
|
|
Quaternion targetRotation = Quaternion.LookRotation(move);
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);
|
|
}
|
|
_controller.Move(move * moveSpeed * Time.deltaTime);
|
|
}
|
|
_animator.SetFloat("MoveSpeed", isAttacking && _isGrounded ? 0 : move.magnitude);
|
|
}
|
|
|
|
private void HandleGravity()
|
|
{
|
|
if (_isGrounded && _velocity.y < 0) _velocity.y = -2f;
|
|
_velocity.y += gravity * Time.deltaTime;
|
|
_controller.Move(_velocity * Time.deltaTime);
|
|
}
|
|
|
|
private void OnJump()
|
|
{
|
|
if (_isGrounded) _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
|
}
|
|
|
|
// 1. 액션 (좌클릭) - 대상이 없어도 나감
|
|
// PlayerNetworkController.cs 중 일부
|
|
private void OnActionInput()
|
|
{
|
|
if (!IsOwner || _actionHandler.IsBusy) return;
|
|
|
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
|
|
|
// 로그 1: 아이템 확인
|
|
if (selectedItem == null) { Debug.Log("선택된 아이템이 없음"); return; }
|
|
|
|
// 로그 2: 도구 여부 및 액션 데이터 확인
|
|
Debug.Log($"현재 아이템: {selectedItem.itemName}, 도구여부: {selectedItem.isTool}, 액션데이터: {selectedItem.toolAction != null}");
|
|
|
|
if (selectedItem.isTool && selectedItem.toolAction != null)
|
|
{
|
|
if (_lastHighlightedBlock != null)
|
|
{
|
|
Debug.Log($"채광 시작: {_lastHighlightedBlock.name}");
|
|
_actionHandler.PerformAction(selectedItem.toolAction, _lastHighlightedBlock.gameObject);
|
|
}
|
|
else
|
|
{
|
|
Debug.Log("조준된 블록이 없음 (하이라이트 확인 필요)");
|
|
_actionHandler.PerformAction(selectedItem.toolAction, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. 인터랙션 (F키) - 대상이 없으면 아예 시작 안 함
|
|
private void OnInteractTap()
|
|
{
|
|
if (!IsOwner || _actionHandler.IsBusy) return;
|
|
|
|
IInteractable target = GetClosestInteractable();
|
|
if (target != null)
|
|
{
|
|
_actionHandler.PerformInteraction(target);
|
|
}
|
|
}
|
|
|
|
[Rpc(SendTo.Server)]
|
|
private void ApplyMiningDamageServerRpc(ulong targetId)
|
|
{
|
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var target))
|
|
{
|
|
if (target.TryGetComponent<MineableBlock>(out var block))
|
|
{
|
|
// 서버에서 최종 거리 검증 후 대미지 적용
|
|
if (Vector3.Distance(transform.position, target.transform.position) <= attackRange + 1.0f)
|
|
{
|
|
block.TakeDamageRpc(miningDamage);
|
|
block.PlayHitEffectClientRpc();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[ClientRpc]
|
|
private void OnAttackClientRpc() => _animator.SetTrigger("Attack");
|
|
|
|
|
|
// 건설 지원 로직 (범위 내 지속 작업이므로 OverlapSphere 유지 또는 Raycast로 변경 가능)
|
|
private void PerformConstructionSupport()
|
|
{
|
|
Collider[] targets = Physics.OverlapSphere(transform.position, interactRange, constructionLayer);
|
|
foreach (var col in targets)
|
|
{
|
|
ConstructionSite site = col.GetComponentInParent<ConstructionSite>();
|
|
if (site != null)
|
|
{
|
|
site.AdvanceConstruction(Time.deltaTime * buildSpeedMultiplier);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
if (IsOwner && _inputActions != null) _inputActions.Disable();
|
|
}
|
|
|
|
private void UpdateCrosshairPosition()
|
|
{
|
|
// 1. 변수 할당 확인 (할당이 안 되어 있으면 여기서 찾음)
|
|
if (_crosshairRect == null)
|
|
{
|
|
GameObject go = GameObject.Find("Crosshair");
|
|
if (go != null) _crosshairRect = go.GetComponent<RectTransform>();
|
|
else return; // 여전히 없으면 중단
|
|
}
|
|
|
|
// 2. 마우스 입력 읽기 (New Input System)
|
|
Vector2 mousePos = Mouse.current.position.ReadValue();
|
|
Vector2 screenCenter = new Vector2(Screen.width / 2f, Screen.height / 2f);
|
|
|
|
// 3. 중앙으로부터의 거리 계산
|
|
Vector2 offset = mousePos - screenCenter;
|
|
|
|
// 4. [중요] 반지름 제한 확인 (crosshairScreenRadius가 0이면 이동하지 않음)
|
|
if (crosshairScreenRadius > 0)
|
|
{
|
|
// 내적과 크기 계산을 통해 원형 제한 적용
|
|
if (offset.magnitude > crosshairScreenRadius)
|
|
{
|
|
offset = offset.normalized * crosshairScreenRadius;
|
|
}
|
|
}
|
|
|
|
// 5. UI 좌표 적용 (Screen Space - Overlay 기준)
|
|
_crosshairRect.position = screenCenter + offset;
|
|
}
|
|
private void UpdateTargetFeedback()
|
|
{
|
|
if (!IsOwner || _crosshairRect == null) return;
|
|
|
|
// 1. 카메라 레이로 조준점 계산 (플레이어 몸통 무시)
|
|
Ray aimRay = Camera.main.ScreenPointToRay(_crosshairRect.position);
|
|
Vector3 worldAimPoint;
|
|
if (Physics.Raycast(aimRay, out RaycastHit mouseHit, 100f, ~ignoreDuringAim))
|
|
worldAimPoint = mouseHit.point;
|
|
else
|
|
worldAimPoint = aimRay.GetPoint(50f);
|
|
|
|
// 2. 캐릭터 가슴에서 조준점을 향하는 방향 계산
|
|
Vector3 origin = transform.position + Vector3.up * 1.2f;
|
|
Vector3 direction = (worldAimPoint - origin).normalized;
|
|
|
|
// 자기 자신 충돌 방지용 오프셋
|
|
Vector3 rayStart = origin + direction * 0.4f;
|
|
|
|
// 3. [중요] 실제 공격과 동일한 SphereCast 실행
|
|
RaycastHit blockHit;
|
|
bool hasTarget = Physics.SphereCast(rayStart, aimRadius, direction, out blockHit, attackRange - 0.4f, mineableLayer);
|
|
|
|
// 4. 하이라이트 대상 업데이트
|
|
MineableBlock currentTarget = null;
|
|
if (hasTarget)
|
|
{
|
|
currentTarget = blockHit.collider.GetComponentInParent<MineableBlock>();
|
|
}
|
|
|
|
// 대상이 바뀌었을 때만 아웃라인 갱신 (최적화)
|
|
if (_lastHighlightedBlock != currentTarget)
|
|
{
|
|
if (_lastHighlightedBlock != null) _lastHighlightedBlock.SetHighlight(false);
|
|
if (currentTarget != null) currentTarget.SetHighlight(true);
|
|
_lastHighlightedBlock = currentTarget;
|
|
}
|
|
|
|
// 기즈모 디버그 데이터 동기화
|
|
_debugOrigin = rayStart;
|
|
_debugDir = direction;
|
|
_debugHit = hasTarget;
|
|
_debugDist = hasTarget ? blockHit.distance : (attackRange - 0.4f);
|
|
|
|
// 크로스헤어 이미지 교체
|
|
if (crosshairUI != null)
|
|
{
|
|
crosshairUI.sprite = hasTarget ? targetCrosshair : idleCrosshair;
|
|
crosshairUI.color = hasTarget ? Color.green : Color.white;
|
|
}
|
|
}
|
|
|
|
private void UpdateBlockVisuals(bool hasTarget, RaycastHit hit)
|
|
{
|
|
MineableBlock newTarget = null;
|
|
|
|
if (hasTarget)
|
|
{
|
|
// 부모나 자신에게서 MineableBlock 컴포넌트를 찾습니다.
|
|
newTarget = hit.collider.GetComponentInParent<MineableBlock>();
|
|
}
|
|
|
|
// 대상이 바뀌었을 때만 실행 (최적화)
|
|
if (_currentTargetBlock != newTarget)
|
|
{
|
|
// 1. 이전 타겟 하이라이트 해제
|
|
if (_currentTargetBlock != null)
|
|
{
|
|
_currentTargetBlock.SetHighlight(false);
|
|
}
|
|
|
|
// 2. 새로운 타겟 하이라이트 적용
|
|
if (newTarget != null)
|
|
{
|
|
newTarget.SetHighlight(true);
|
|
}
|
|
|
|
_currentTargetBlock = newTarget;
|
|
}
|
|
|
|
// 3. 크로스헤어 상태 업데이트
|
|
if (crosshairUI != null)
|
|
{
|
|
crosshairUI.sprite = hasTarget ? targetCrosshair : idleCrosshair;
|
|
crosshairUI.color = hasTarget ? Color.green : Color.white;
|
|
}
|
|
}
|
|
|
|
private void RevealSurroundings()
|
|
{
|
|
// 시야 반경 내의 블록 감지
|
|
Collider[] hitBlocks = Physics.OverlapSphere(transform.position, visionRadius, mineableLayer);
|
|
|
|
foreach (var col in hitBlocks)
|
|
{
|
|
if (col.TryGetComponent<MineableBlock>(out var block))
|
|
{
|
|
// 1. [로컬] 내 화면에서 이 블록을 보이게 함 (실시간 시야)
|
|
block.UpdateLocalVisibility();
|
|
|
|
// 2. [서버] 아직 발견 안 된 블록이라면 서버에 알림 (공유 안개 해제)
|
|
if (IsOwner)
|
|
{
|
|
RequestRevealServerRpc(block.GetComponent<NetworkObject>().NetworkObjectId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[ServerRpc]
|
|
private void RequestRevealServerRpc(ulong blockNetId)
|
|
{
|
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(blockNetId, out var netObj))
|
|
{
|
|
if (netObj.TryGetComponent<MineableBlock>(out var block))
|
|
{
|
|
block.RevealBlock(); // 서버가 블록 상태를 true로 변경
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerator ActionRoutine(float duration, string animTrigger, Action actionLogic)
|
|
{
|
|
// 1. 상태 잠금
|
|
_currentState = ActionState.Busy;
|
|
|
|
// 2. 애니메이션 실행
|
|
if (!string.IsNullOrEmpty(animTrigger))
|
|
{
|
|
_animator.SetTrigger(animTrigger);
|
|
}
|
|
|
|
// 3. 실제 로직 실행 (상호작용, 아이템 사용 등)
|
|
actionLogic?.Invoke();
|
|
|
|
// 4. 지정된 시간만큼 대기
|
|
yield return new WaitForSeconds(duration);
|
|
|
|
// 5. 상태 해제
|
|
_currentState = ActionState.Idle;
|
|
}
|
|
|
|
private IInteractable GetClosestInteractable()
|
|
{
|
|
// 1. 지정된 레이어 내의 콜라이더 탐색
|
|
Collider[] colliders = Physics.OverlapSphere(transform.position, interactRange, interactableLayer);
|
|
|
|
IInteractable closest = null;
|
|
float minDistance = float.MaxValue;
|
|
|
|
foreach (var col in colliders)
|
|
{
|
|
// 2. IInteractable 인터페이스를 가지고 있는지 확인 (부모 포함)
|
|
IInteractable interactable = col.GetComponentInParent<IInteractable>();
|
|
|
|
if (interactable != null)
|
|
{
|
|
float dist = Vector3.Distance(transform.position, col.transform.position);
|
|
if (dist < minDistance)
|
|
{
|
|
minDistance = dist;
|
|
closest = interactable;
|
|
}
|
|
}
|
|
}
|
|
|
|
return closest;
|
|
}
|
|
|
|
private void HandleContinuousAction()
|
|
{
|
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
|
if (selectedItem == null || !selectedItem.isTool || selectedItem.toolAction == null) return;
|
|
|
|
// [핵심] 반복 가능한 액션일 때만 Update에서 재실행
|
|
if (selectedItem.toolAction.canRepeat)
|
|
{
|
|
TryExecuteAction();
|
|
}
|
|
}
|
|
|
|
private void TryExecuteAction()
|
|
{
|
|
if (_actionHandler.IsBusy) return;
|
|
|
|
ItemData selectedItem = _inventory.GetSelectedItemData();
|
|
if (selectedItem != null && selectedItem.isTool && selectedItem.toolAction != null)
|
|
{
|
|
// 단발성 액션인데 이미 한 번 실행했다면 스킵
|
|
if (!selectedItem.toolAction.canRepeat && _hasExecutedOnce) return;
|
|
|
|
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
|
|
_actionHandler.PerformAction(selectedItem.toolAction, target);
|
|
|
|
_hasExecutedOnce = true; // 실행 기록 저장
|
|
}
|
|
}
|
|
|
|
private void OnDrawGizmos()
|
|
{
|
|
if (!Application.isPlaying || !IsOwner) return;
|
|
|
|
// 실제 채굴 탐색 궤적을 씬 뷰에 표시
|
|
Gizmos.color = _debugHit ? Color.red : Color.green;
|
|
|
|
// 광선 표시
|
|
Gizmos.DrawLine(_debugOrigin, _debugOrigin + _debugDir * _debugDist);
|
|
|
|
// 탐색 영역(구체) 표시
|
|
Gizmos.DrawWireSphere(_debugOrigin + _debugDir * _debugDist, aimRadius);
|
|
}
|
|
} |