블록 조준 시스템 제작
및 기본 에디터 자동 호스트화
This commit is contained in:
@@ -21,9 +21,19 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
[SerializeField] private float buildSpeedMultiplier = 2f;
|
||||
|
||||
[Header("Mining Settings")]
|
||||
[SerializeField] private float attackRange = 1.5f;
|
||||
[SerializeField] private int miningDamage = 25;
|
||||
[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;
|
||||
private RectTransform _crosshairRect;
|
||||
private MineableBlock _currentTargetBlock; // 현재 강조 중인 블록 저장
|
||||
|
||||
private CharacterController _controller;
|
||||
private PlayerInputActions _inputActions;
|
||||
@@ -36,6 +46,12 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
private bool _isGrounded;
|
||||
private bool _isHoldingInteract = false;
|
||||
|
||||
// 디버그 변수
|
||||
private Vector3 _debugOrigin;
|
||||
private Vector3 _debugDir;
|
||||
private bool _debugHit;
|
||||
private float _debugDist;
|
||||
|
||||
// NGO 초기화
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
@@ -53,9 +69,19 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
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.Attack.performed += ctx => OnAttackServerRpc();
|
||||
_inputActions.Player.Attack.performed += ctx => OnAttackInput();
|
||||
_inputActions.Player.Interact.performed += ctx => OnInteractTap(); // 탭 상호작용
|
||||
|
||||
_inputActions.Player.Interact.started += ctx => _isHoldingInteract = true;
|
||||
@@ -80,6 +106,9 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
HandleMovement();
|
||||
|
||||
if (_isHoldingInteract) PerformConstructionSupport();
|
||||
|
||||
UpdateCrosshairPosition(); // 이 안에서 움직여야 합니다.
|
||||
UpdateTargetFeedback();
|
||||
}
|
||||
|
||||
// --- 이동 관련 로직 (기존 유지) ---
|
||||
@@ -119,16 +148,52 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
if (_isGrounded) _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||
}
|
||||
|
||||
// --- 채광 로직 (기존 유지) ---
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||
private void OnAttackServerRpc()
|
||||
private void OnAttackInput()
|
||||
{
|
||||
OnAttackClientRpc();
|
||||
Collider[] hitBlocks = Physics.OverlapSphere(transform.position + transform.forward, attackRange, mineableLayer);
|
||||
foreach (var col in hitBlocks)
|
||||
if (!IsOwner) return;
|
||||
|
||||
// 1. 마우스가 가리키는 월드상의 위치를 먼저 찾습니다.
|
||||
Ray mouseRay = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
|
||||
Vector3 worldAimPoint;
|
||||
|
||||
// 아주 멀리까지 레이를 쏴서 조준 방향을 결정합니다.
|
||||
if (Physics.Raycast(mouseRay, out RaycastHit mouseHit, 1000f, ~ignoreDuringAim))
|
||||
worldAimPoint = mouseHit.point;
|
||||
else
|
||||
worldAimPoint = mouseRay.GetPoint(100f);
|
||||
|
||||
// 2. 캐릭터 가슴 높이에서 조준점을 향하는 방향 계산
|
||||
Vector3 origin = transform.position + Vector3.up * 1.2f;
|
||||
Vector3 direction = (worldAimPoint - origin).normalized;
|
||||
|
||||
// 3. 캐릭터에서 해당 방향으로 SphereCast를 쏴서 가장 가까운 블록 하나를 찾습니다.
|
||||
// SphereCast는 가장 먼저 닿는 오브젝트 하나만 hit에 담습니다.
|
||||
if (Physics.SphereCast(origin, aimRadius, direction, out RaycastHit blockHit, attackRange, mineableLayer))
|
||||
{
|
||||
MineableBlock block = col.GetComponentInParent<MineableBlock>();
|
||||
if (block != null) block.TakeDamageRpc(miningDamage);
|
||||
if (blockHit.collider.TryGetComponent<NetworkObject>(out var netObj))
|
||||
{
|
||||
// 서버에 대미지 요청
|
||||
ApplyMiningDamageServerRpc(netObj.NetworkObjectId);
|
||||
}
|
||||
}
|
||||
|
||||
_animator.SetTrigger("Attack");
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,4 +251,132 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
{
|
||||
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 cameraRay = Camera.main.ScreenPointToRay(_crosshairRect.position);
|
||||
RaycastHit cameraHit;
|
||||
|
||||
// 지형이나 블록을 모두 검사하여 조준점을 잡습니다.
|
||||
bool hitSomething = Physics.Raycast(cameraRay, out cameraHit, 150f, ~ignoreDuringAim);
|
||||
Vector3 worldAimPoint = hitSomething ? cameraHit.point : cameraRay.GetPoint(100f);
|
||||
|
||||
// 2. [거리 및 방향 계산] 캐릭터 가슴에서 그 지점까지의 벡터를 구합니다.
|
||||
Vector3 origin = transform.position + Vector3.up * 1.2f;
|
||||
Vector3 toTarget = worldAimPoint - origin;
|
||||
float distToTarget = toTarget.magnitude;
|
||||
Vector3 direction = toTarget.normalized;
|
||||
|
||||
// 3. [대상 우선 판정]
|
||||
// 만약 카메라 레이가 '채광 가능한 블록'을 직접 때렸고, 그게 사거리 이내라면? -> 바로 타겟팅!
|
||||
bool isDirectHit = hitSomething && ((1 << cameraHit.collider.gameObject.layer) & mineableLayer) != 0;
|
||||
|
||||
RaycastHit finalHit;
|
||||
bool hasValidTarget = false;
|
||||
|
||||
if (isDirectHit && distToTarget <= attackRange)
|
||||
{
|
||||
// 마우스가 직접 블록을 가리키고 사거리 내에 있는 경우
|
||||
finalHit = cameraHit;
|
||||
hasValidTarget = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 마우스가 허공을 보거나 너무 먼 곳을 볼 때만 '범위 탐색(SphereCast)'을 수행합니다.
|
||||
float searchDist = Mathf.Min(distToTarget, attackRange);
|
||||
Vector3 rayStart = origin + direction * 0.4f;
|
||||
hasValidTarget = Physics.SphereCast(rayStart, aimRadius, direction, out finalHit, searchDist - 0.4f, mineableLayer);
|
||||
}
|
||||
|
||||
// 4. 디버그 및 시각화 업데이트
|
||||
_debugOrigin = origin;
|
||||
_debugDir = direction;
|
||||
_debugHit = hasValidTarget;
|
||||
_debugDist = hasValidTarget ? Vector3.Distance(origin, finalHit.point) : Mathf.Min(distToTarget, attackRange);
|
||||
|
||||
UpdateBlockVisuals(hasValidTarget, finalHit);
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user