ADD: 드론 카메라 게임패드 제어 시스템
게임패드(Xbox/PS)를 이용한 6DOF 드론 카메라 자유비행 모드 추가 - GamepadInputHandler: 게임패드 입력 처리 (스틱, 트리거, 버튼, D-pad) - DroneCameraMode: 관성 기반 드론 물리 시뮬레이션 및 타겟 자동추적 - CameraController: 드론 모드 토글, 프리셋별 드론 상태 저장/복원 - SystemController: 아바타 Head 콜라이더 자동 생성 및 관리 - StreamDeckServerManager: 드론 모드 WebSocket 연동 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
00ba416b74
commit
efc0adced8
@ -310,6 +310,14 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
HandleGetCameraList(service);
|
||||
break;
|
||||
|
||||
case "toggle_drone_mode":
|
||||
HandleToggleDroneMode(service);
|
||||
break;
|
||||
|
||||
case "get_drone_state":
|
||||
HandleGetDroneState(service);
|
||||
break;
|
||||
|
||||
case "toggle_item":
|
||||
HandleToggleItem(message);
|
||||
break;
|
||||
@ -493,6 +501,36 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
service.SendMessage(json);
|
||||
}
|
||||
|
||||
private void HandleToggleDroneMode(StreamDeckService service)
|
||||
{
|
||||
if (cameraManager == null) return;
|
||||
|
||||
cameraManager.ToggleDroneMode();
|
||||
|
||||
// 상태 응답
|
||||
HandleGetDroneState(service);
|
||||
}
|
||||
|
||||
private void HandleGetDroneState(StreamDeckService service)
|
||||
{
|
||||
if (cameraManager == null) return;
|
||||
|
||||
var response = new
|
||||
{
|
||||
type = "drone_state_response",
|
||||
timestamp = DateTime.UtcNow.ToString("o"),
|
||||
version = "1.0",
|
||||
data = new
|
||||
{
|
||||
is_drone_mode = cameraManager.IsDroneModeActive,
|
||||
current_camera = cameraManager.GetCurrentCameraState()
|
||||
}
|
||||
};
|
||||
|
||||
string json = JsonConvert.SerializeObject(response);
|
||||
service.SendMessage(json);
|
||||
}
|
||||
|
||||
private void HandleToggleItem(Dictionary<string, object> message)
|
||||
{
|
||||
Debug.Log($"[StreamDeckServerManager] 아이템 토글 요청 수신");
|
||||
|
||||
@ -923,41 +923,39 @@ public class CameraControlSystem : MonoBehaviour
|
||||
{
|
||||
availableDOFTargets.Clear();
|
||||
|
||||
// 씬의 모든 CustomRetargetingScript 찾기
|
||||
var retargetingScripts = FindObjectsByType<KindRetargeting.CustomRetargetingScript>(FindObjectsSortMode.None);
|
||||
Debug.Log($"[CameraControlSystem] CustomRetargetingScript {retargetingScripts.Length}개 발견");
|
||||
// SystemController에서 글로벌하게 관리하는 Head 타겟 목록 가져오기
|
||||
var systemController = SystemController.Instance;
|
||||
if (systemController != null)
|
||||
{
|
||||
var headTargets = systemController.GetAvatarHeadTargets();
|
||||
if (headTargets != null && headTargets.Count > 0)
|
||||
{
|
||||
availableDOFTargets.AddRange(headTargets);
|
||||
Debug.Log($"[CameraControlSystem] SystemController에서 DOF 타겟 {availableDOFTargets.Count}개 가져옴");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// SystemController가 없거나 타겟이 비어있으면 직접 검색 (폴백)
|
||||
var retargetingScripts = FindObjectsByType<KindRetargeting.CustomRetargetingScript>(FindObjectsSortMode.None);
|
||||
foreach (var script in retargetingScripts)
|
||||
{
|
||||
Debug.Log($"[CameraControlSystem] 스크립트 검사 중: {script.gameObject.name}, targetAnimator = {script.targetAnimator}");
|
||||
if (script.targetAnimator == null) continue;
|
||||
|
||||
if (script.targetAnimator != null)
|
||||
{
|
||||
// Head 본 찾기
|
||||
Transform headBone = script.targetAnimator.GetBoneTransform(HumanBodyBones.Head);
|
||||
Debug.Log($"[CameraControlSystem] Head 본 찾기: {(headBone != null ? headBone.name : "null")}");
|
||||
if (headBone == null) continue;
|
||||
|
||||
if (headBone != null)
|
||||
{
|
||||
// Head에 콜라이더가 없으면 추가
|
||||
bool hasCollider = headBone.TryGetComponent<SphereCollider>(out _);
|
||||
Debug.Log($"[CameraControlSystem] {headBone.name}에 기존 콜라이더: {hasCollider}");
|
||||
|
||||
if (!hasCollider)
|
||||
if (!headBone.TryGetComponent<SphereCollider>(out _))
|
||||
{
|
||||
var collider = headBone.gameObject.AddComponent<SphereCollider>();
|
||||
collider.radius = 0.1f;
|
||||
collider.isTrigger = true;
|
||||
Debug.Log($"[CameraControlSystem] {headBone.name}에 SphereCollider 추가 완료 (radius: 0.1)");
|
||||
}
|
||||
|
||||
availableDOFTargets.Add(headBone);
|
||||
Debug.Log($"[CameraControlSystem] DOF 타겟 추가: {headBone.name} ({script.targetAnimator.gameObject.name})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[CameraControlSystem] 총 {availableDOFTargets.Count}개의 DOF 타겟 발견");
|
||||
Debug.Log($"[CameraControlSystem] DOF 타겟 {availableDOFTargets.Count}개 발견 (폴백)");
|
||||
}
|
||||
|
||||
private void CycleDOFTarget()
|
||||
|
||||
@ -0,0 +1,435 @@
|
||||
using UnityEngine;
|
||||
using Unity.Cinemachine;
|
||||
|
||||
/// <summary>
|
||||
/// 드론 카메라 자유비행(6DOF) 모드.
|
||||
/// GamepadInputHandler의 입력을 받아 Cinemachine 카메라를 직접 이동/회전합니다.
|
||||
/// 관성 기반 물리 모델로 자연스러운 드론 느낌을 구현합니다.
|
||||
/// </summary>
|
||||
public class DroneCameraMode : MonoBehaviour
|
||||
{
|
||||
[Header("Movement Physics")]
|
||||
[SerializeField] private float acceleration = 5.0f;
|
||||
[SerializeField] private float drag = 3.0f;
|
||||
[SerializeField] private float maxSpeed = 10f;
|
||||
|
||||
[Header("Rotation")]
|
||||
[SerializeField] private float yawSpeed = 90f;
|
||||
[SerializeField] private float pitchSpeed = 60f;
|
||||
[SerializeField] private float rotationSmoothing = 0.1f;
|
||||
|
||||
[Header("FOV Control")]
|
||||
[SerializeField] private float fovSpeed = 30f;
|
||||
[SerializeField] private float minFOV = 10f;
|
||||
[SerializeField] private float maxFOV = 90f;
|
||||
|
||||
[Header("Targeting")]
|
||||
[SerializeField] private float raycastDistance = 100f;
|
||||
[SerializeField] private float sphereCastRadius = 0.5f; // SphereCast 반경 (넓게 잡음)
|
||||
|
||||
// 상태
|
||||
private bool isActive;
|
||||
private Vector3 velocity;
|
||||
private float currentYaw;
|
||||
private float currentPitch;
|
||||
private Quaternion targetRotation;
|
||||
|
||||
// 타겟팅 (Head 본이 아닌 캐릭터 단위)
|
||||
private int currentTargetIndex = -1;
|
||||
private Transform currentTarget;
|
||||
public Transform CurrentTarget => currentTarget;
|
||||
|
||||
// 참조
|
||||
private GamepadInputHandler gamepadInput;
|
||||
private CinemachineCamera targetCamera;
|
||||
|
||||
// 드론 모드 진입 전 상태 저장
|
||||
private Transform savedFollow;
|
||||
private Transform savedLookAt;
|
||||
private Vector3 savedPosition;
|
||||
private Quaternion savedRotation;
|
||||
private float savedFOV;
|
||||
|
||||
public bool IsActive => isActive;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
gamepadInput = GetComponent<GamepadInputHandler>();
|
||||
if (gamepadInput == null)
|
||||
gamepadInput = gameObject.AddComponent<GamepadInputHandler>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드론 모드 활성화. 대상 Cinemachine 카메라의 Follow/LookAt을 해제하고 직접 제어를 시작합니다.
|
||||
/// </summary>
|
||||
public void Activate(CinemachineCamera camera)
|
||||
{
|
||||
if (camera == null || isActive) return;
|
||||
|
||||
targetCamera = camera;
|
||||
|
||||
// 현재 상태 저장
|
||||
savedFollow = camera.Follow;
|
||||
savedLookAt = camera.LookAt;
|
||||
savedPosition = camera.transform.position;
|
||||
savedRotation = camera.transform.rotation;
|
||||
savedFOV = camera.Lens.FieldOfView;
|
||||
|
||||
// Cinemachine 추적 해제 → 직접 제어
|
||||
camera.Follow = null;
|
||||
camera.LookAt = null;
|
||||
|
||||
// 현재 회전을 오일러로 분해
|
||||
Vector3 euler = camera.transform.eulerAngles;
|
||||
currentYaw = euler.y;
|
||||
currentPitch = euler.x;
|
||||
if (currentPitch > 180f) currentPitch -= 360f; // -180~180 범위로
|
||||
|
||||
targetRotation = camera.transform.rotation;
|
||||
velocity = Vector3.zero;
|
||||
isActive = true;
|
||||
|
||||
Debug.Log($"[DroneCameraMode] 활성화 - 카메라: {camera.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드론 모드 비활성화. Follow/LookAt을 복원합니다.
|
||||
/// </summary>
|
||||
public void Deactivate()
|
||||
{
|
||||
if (!isActive || targetCamera == null) return;
|
||||
|
||||
// Follow/LookAt 복원
|
||||
targetCamera.Follow = savedFollow;
|
||||
targetCamera.LookAt = savedLookAt;
|
||||
|
||||
velocity = Vector3.zero;
|
||||
isActive = false;
|
||||
|
||||
Debug.Log("[DroneCameraMode] 비활성화");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드론 모드 비활성화 + 원래 위치로 복귀
|
||||
/// </summary>
|
||||
public void DeactivateAndRestore()
|
||||
{
|
||||
if (!isActive || targetCamera == null) return;
|
||||
|
||||
targetCamera.transform.position = savedPosition;
|
||||
targetCamera.transform.rotation = savedRotation;
|
||||
|
||||
var lens = targetCamera.Lens;
|
||||
lens.FieldOfView = savedFOV;
|
||||
targetCamera.Lens = lens;
|
||||
|
||||
Deactivate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 위치를 새 홈 포지션으로 저장
|
||||
/// </summary>
|
||||
public void SaveCurrentAsHome()
|
||||
{
|
||||
if (!isActive || targetCamera == null) return;
|
||||
|
||||
savedPosition = targetCamera.transform.position;
|
||||
savedRotation = targetCamera.transform.rotation;
|
||||
savedFOV = targetCamera.Lens.FieldOfView;
|
||||
|
||||
Debug.Log("[DroneCameraMode] 현재 위치를 홈으로 저장");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 홈 포지션으로 복귀 (드론 모드 유지)
|
||||
/// </summary>
|
||||
public void ReturnToHome()
|
||||
{
|
||||
if (!isActive || targetCamera == null) return;
|
||||
|
||||
targetCamera.transform.position = savedPosition;
|
||||
targetCamera.transform.rotation = savedRotation;
|
||||
|
||||
Vector3 euler = savedRotation.eulerAngles;
|
||||
currentYaw = euler.y;
|
||||
currentPitch = euler.x;
|
||||
if (currentPitch > 180f) currentPitch -= 360f;
|
||||
|
||||
targetRotation = savedRotation;
|
||||
velocity = Vector3.zero;
|
||||
|
||||
var lens = targetCamera.Lens;
|
||||
lens.FieldOfView = savedFOV;
|
||||
targetCamera.Lens = lens;
|
||||
|
||||
Debug.Log("[DroneCameraMode] 홈 위치로 복귀");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드론 상태를 직렬화 가능한 데이터로 반환
|
||||
/// </summary>
|
||||
public DroneStateData GetState()
|
||||
{
|
||||
return new DroneStateData
|
||||
{
|
||||
isActive = isActive,
|
||||
position = isActive && targetCamera != null ? targetCamera.transform.position : savedPosition,
|
||||
rotation = isActive && targetCamera != null ? targetCamera.transform.eulerAngles : savedRotation.eulerAngles,
|
||||
fov = isActive && targetCamera != null ? targetCamera.Lens.FieldOfView : savedFOV,
|
||||
speed = velocity.magnitude,
|
||||
targetName = currentTarget != null ? currentTarget.name : null
|
||||
};
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!isActive || targetCamera == null || gamepadInput == null) return;
|
||||
|
||||
float dt = Time.deltaTime;
|
||||
|
||||
// === 회전 ===
|
||||
bool hasStickInput = gamepadInput.RightStick.sqrMagnitude > 0.01f;
|
||||
|
||||
if (currentTarget != null && !hasStickInput)
|
||||
{
|
||||
// 타겟 자동 추적: 카메라가 타겟을 부드럽게 바라봄
|
||||
Vector3 dirToTarget = currentTarget.position - targetCamera.transform.position;
|
||||
if (dirToTarget.sqrMagnitude > 0.001f)
|
||||
{
|
||||
Quaternion lookRot = Quaternion.LookRotation(dirToTarget);
|
||||
targetCamera.transform.rotation = Quaternion.Slerp(
|
||||
targetCamera.transform.rotation,
|
||||
lookRot,
|
||||
1f - Mathf.Exp(-dt / Mathf.Max(rotationSmoothing, 0.001f))
|
||||
);
|
||||
// yaw/pitch 동기화 (스틱 조작 시 이어서 진행되도록)
|
||||
Vector3 euler = targetCamera.transform.eulerAngles;
|
||||
currentYaw = euler.y;
|
||||
currentPitch = euler.x;
|
||||
if (currentPitch > 180f) currentPitch -= 360f;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 수동 회전: 우스틱으로 조작
|
||||
currentYaw += gamepadInput.RightStick.x * yawSpeed * dt;
|
||||
currentPitch -= gamepadInput.RightStick.y * pitchSpeed * dt;
|
||||
currentPitch = Mathf.Clamp(currentPitch, -85f, 85f);
|
||||
|
||||
targetRotation = Quaternion.Euler(currentPitch, currentYaw, 0f);
|
||||
targetCamera.transform.rotation = Quaternion.Slerp(
|
||||
targetCamera.transform.rotation,
|
||||
targetRotation,
|
||||
1f - Mathf.Exp(-dt / Mathf.Max(rotationSmoothing, 0.001f))
|
||||
);
|
||||
}
|
||||
|
||||
// === 이동 ===
|
||||
Vector3 inputDir = Vector3.zero;
|
||||
|
||||
// 좌스틱 Y → 전진/후진
|
||||
inputDir += targetCamera.transform.forward * gamepadInput.LeftStick.y;
|
||||
|
||||
// 좌스틱 X → 좌우 이동
|
||||
inputDir += targetCamera.transform.right * gamepadInput.LeftStick.x;
|
||||
|
||||
// RT → 상승, LT → 하강
|
||||
float verticalInput = gamepadInput.RightTrigger - gamepadInput.LeftTrigger;
|
||||
inputDir += Vector3.up * verticalInput;
|
||||
|
||||
// 물리: 가속 + 감쇠
|
||||
velocity += inputDir * (acceleration * dt);
|
||||
velocity -= velocity * (drag * dt);
|
||||
|
||||
// 최대 속도 제한
|
||||
float currentMaxSpeed = maxSpeed;
|
||||
if (velocity.magnitude > currentMaxSpeed)
|
||||
velocity = velocity.normalized * currentMaxSpeed;
|
||||
|
||||
targetCamera.transform.position += velocity * dt;
|
||||
|
||||
// === FOV ===
|
||||
if (gamepadInput.LeftShoulderHeld || gamepadInput.RightShoulderHeld)
|
||||
{
|
||||
float fovDelta = 0f;
|
||||
if (gamepadInput.LeftShoulderHeld) fovDelta -= fovSpeed * dt; // LB: 줌 인 (FOV 감소)
|
||||
if (gamepadInput.RightShoulderHeld) fovDelta += fovSpeed * dt; // RB: 줌 아웃 (FOV 증가)
|
||||
|
||||
var lens = targetCamera.Lens;
|
||||
lens.FieldOfView = Mathf.Clamp(lens.FieldOfView + fovDelta, minFOV, maxFOV);
|
||||
targetCamera.Lens = lens;
|
||||
}
|
||||
|
||||
// 타겟팅은 CameraController.HandleGamepadButtons()에서 처리
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 외부에서 호출: 카메라 중앙 SphereCast로 타겟팅
|
||||
/// </summary>
|
||||
public void TryTargetFromCamera()
|
||||
{
|
||||
TryRaycastTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 외부에서 호출: 캐릭터 목록 순환
|
||||
/// </summary>
|
||||
public void CycleTargetPublic(int direction)
|
||||
{
|
||||
CycleTarget(direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카메라 중앙에서 SphereCast를 발사해 가장 가까운 캐릭터를 타겟팅합니다.
|
||||
/// </summary>
|
||||
private void TryRaycastTarget()
|
||||
{
|
||||
Camera mainCam = Camera.main;
|
||||
if (mainCam == null) return;
|
||||
|
||||
var headTargets = GetHeadTargets();
|
||||
if (headTargets == null || headTargets.Count == 0)
|
||||
{
|
||||
// 타겟이 없으면 SystemController에서 새로고침 시도
|
||||
var sc = SystemController.Instance;
|
||||
if (sc != null)
|
||||
{
|
||||
sc.RefreshAvatarHeadColliders();
|
||||
headTargets = sc.GetAvatarHeadTargets();
|
||||
if (headTargets == null || headTargets.Count == 0) return;
|
||||
}
|
||||
else return;
|
||||
}
|
||||
|
||||
Ray ray = mainCam.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
|
||||
RaycastHit[] hits = Physics.SphereCastAll(ray, sphereCastRadius, raycastDistance);
|
||||
|
||||
Transform closestHead = null;
|
||||
float closestDist = float.MaxValue;
|
||||
int closestIndex = -1;
|
||||
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
// Head 타겟 목록에 있는지 확인 (콜라이더 종류 무관)
|
||||
int idx = headTargets.IndexOf(hit.transform);
|
||||
if (idx >= 0 && hit.distance < closestDist)
|
||||
{
|
||||
closestHead = hit.transform;
|
||||
closestDist = hit.distance;
|
||||
closestIndex = idx;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestHead != null)
|
||||
{
|
||||
currentTarget = closestHead;
|
||||
currentTargetIndex = closestIndex;
|
||||
Debug.Log($"[DroneCameraMode] 타겟 설정: {closestHead.name} (거리: {closestDist:F2}m)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("[DroneCameraMode] 타겟을 찾지 못했습니다 - 가장 가까운 캐릭터로 설정합니다");
|
||||
// SphereCast가 안 맞으면 거리 기반으로 가장 가까운 캐릭터 선택
|
||||
SetClosestTarget();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카메라에서 가장 가까운 캐릭터를 타겟으로 설정합니다.
|
||||
/// </summary>
|
||||
private void SetClosestTarget()
|
||||
{
|
||||
var headTargets = GetHeadTargets();
|
||||
if (headTargets == null || headTargets.Count == 0 || targetCamera == null) return;
|
||||
|
||||
Vector3 camPos = targetCamera.transform.position;
|
||||
float closestDist = float.MaxValue;
|
||||
int closestIndex = -1;
|
||||
|
||||
for (int i = 0; i < headTargets.Count; i++)
|
||||
{
|
||||
if (headTargets[i] == null) continue;
|
||||
float dist = Vector3.Distance(camPos, headTargets[i].position);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestIndex >= 0)
|
||||
{
|
||||
currentTargetIndex = closestIndex;
|
||||
currentTarget = headTargets[closestIndex];
|
||||
Debug.Log($"[DroneCameraMode] 가장 가까운 타겟: {currentTarget.name} (거리: {closestDist:F2}m)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 등록된 캐릭터 목록에서 다음/이전 캐릭터로 타겟을 전환합니다.
|
||||
/// </summary>
|
||||
private void CycleTarget(int direction)
|
||||
{
|
||||
var headTargets = GetHeadTargets();
|
||||
if (headTargets == null || headTargets.Count == 0) return;
|
||||
|
||||
if (headTargets.Count == 1)
|
||||
{
|
||||
currentTargetIndex = 0;
|
||||
currentTarget = headTargets[0];
|
||||
Debug.Log($"[DroneCameraMode] 타겟 (유일): {currentTarget.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
currentTargetIndex += direction;
|
||||
|
||||
// 순환
|
||||
if (currentTargetIndex >= headTargets.Count)
|
||||
currentTargetIndex = 0;
|
||||
else if (currentTargetIndex < 0)
|
||||
currentTargetIndex = headTargets.Count - 1;
|
||||
|
||||
currentTarget = headTargets[currentTargetIndex];
|
||||
Debug.Log($"[DroneCameraMode] 타겟 전환 [{currentTargetIndex + 1}/{headTargets.Count}]: {currentTarget.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 외부에서 타겟을 직접 설정합니다.
|
||||
/// </summary>
|
||||
public void SetTarget(Transform target)
|
||||
{
|
||||
currentTarget = target;
|
||||
var headTargets = GetHeadTargets();
|
||||
currentTargetIndex = headTargets != null ? headTargets.IndexOf(target) : -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 타겟을 해제합니다.
|
||||
/// </summary>
|
||||
public void ClearTarget()
|
||||
{
|
||||
if (currentTarget != null)
|
||||
{
|
||||
Debug.Log($"[DroneCameraMode] 타겟 해제: {currentTarget.name}");
|
||||
}
|
||||
currentTarget = null;
|
||||
currentTargetIndex = -1;
|
||||
}
|
||||
|
||||
private System.Collections.Generic.List<Transform> GetHeadTargets()
|
||||
{
|
||||
var systemController = SystemController.Instance;
|
||||
return systemController != null ? systemController.GetAvatarHeadTargets() : null;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class DroneStateData
|
||||
{
|
||||
public bool isActive;
|
||||
public Vector3 position;
|
||||
public Vector3 rotation;
|
||||
public float fov;
|
||||
public float speed;
|
||||
public string targetName;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2e67949346367641b5bbd8b4401f311
|
||||
@ -287,6 +287,22 @@ public class CameraManager : MonoBehaviour, IController
|
||||
private InputHandler inputHandler;
|
||||
private CameraPreset currentPreset;
|
||||
|
||||
// 드론 카메라 모드
|
||||
private DroneCameraMode droneCameraMode;
|
||||
private GamepadInputHandler gamepadInput;
|
||||
|
||||
// 프리셋별 드론 상태 저장
|
||||
private class SavedDroneState
|
||||
{
|
||||
public bool wasActive;
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
public float fov;
|
||||
public Transform target;
|
||||
public int targetIndex;
|
||||
}
|
||||
private Dictionary<CameraPreset, SavedDroneState> savedDroneStates = new Dictionary<CameraPreset, SavedDroneState>();
|
||||
|
||||
// 오빗 카메라 상태 (각도 기반)
|
||||
private float horizontalAngle;
|
||||
private float verticalAngle;
|
||||
@ -365,6 +381,9 @@ public class CameraManager : MonoBehaviour, IController
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 게임패드는 IsValidSetup과 무관하게 항상 처리
|
||||
HandleGamepadButtons();
|
||||
|
||||
if (!IsValidSetup) return;
|
||||
|
||||
UpdateHotkeyRecording();
|
||||
@ -382,6 +401,114 @@ public class CameraManager : MonoBehaviour, IController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleGamepadButtons()
|
||||
{
|
||||
if (gamepadInput == null || !gamepadInput.IsGamepadConnected) return;
|
||||
|
||||
// A 버튼: 드론 모드 토글
|
||||
if (gamepadInput.ButtonSouthPressed)
|
||||
{
|
||||
ToggleDroneMode();
|
||||
}
|
||||
|
||||
// 드론 모드 전용 버튼
|
||||
if (droneCameraMode != null && droneCameraMode.IsActive)
|
||||
{
|
||||
// B 버튼: 현재 위치를 홈으로 저장
|
||||
if (gamepadInput.ButtonEastPressed)
|
||||
{
|
||||
droneCameraMode.SaveCurrentAsHome();
|
||||
}
|
||||
|
||||
// Y 버튼: 홈 위치로 복귀
|
||||
if (gamepadInput.ButtonNorthPressed)
|
||||
{
|
||||
droneCameraMode.ReturnToHome();
|
||||
}
|
||||
}
|
||||
// D-pad 타겟팅 (드론 모드 관계없이 항상 동작)
|
||||
// Update 실행 순서에 의존하지 않도록 Gamepad.current에서 직접 읽음
|
||||
if (droneCameraMode != null)
|
||||
{
|
||||
var gamepad = UnityEngine.InputSystem.Gamepad.current;
|
||||
if (gamepad != null)
|
||||
{
|
||||
if (gamepad.dpad.up.wasPressedThisFrame)
|
||||
droneCameraMode.TryTargetFromCamera();
|
||||
if (gamepad.dpad.down.wasPressedThisFrame)
|
||||
droneCameraMode.ClearTarget();
|
||||
if (gamepad.dpad.right.wasPressedThisFrame)
|
||||
droneCameraMode.CycleTargetPublic(1);
|
||||
if (gamepad.dpad.left.wasPressedThisFrame)
|
||||
droneCameraMode.CycleTargetPublic(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Start 버튼: 다음 카메라 프리셋
|
||||
if (gamepadInput.StartPressed)
|
||||
{
|
||||
int currentIndex = currentPreset != null ? cameraPresets.IndexOf(currentPreset) : -1;
|
||||
int nextIndex = (currentIndex + 1) % cameraPresets.Count;
|
||||
Set(nextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 드론 모드를 토글합니다. 게임패드가 연결되어 있어야 합니다.
|
||||
/// </summary>
|
||||
public void ToggleDroneMode()
|
||||
{
|
||||
if (droneCameraMode == null || currentCamera == null) return;
|
||||
|
||||
if (droneCameraMode.IsActive)
|
||||
{
|
||||
droneCameraMode.Deactivate();
|
||||
|
||||
// orbit 상태 복원
|
||||
if (currentPreset != null && currentPreset.hasOrbitState)
|
||||
{
|
||||
if (currentPreset.TryRestoreOrbitState(out float hAngle, out float vAngle, out float dist, out Vector3 focus, out float fov))
|
||||
{
|
||||
targetHorizontalAngle = hAngle;
|
||||
targetVerticalAngle = vAngle;
|
||||
targetDistance = dist;
|
||||
targetFocusPoint = focus;
|
||||
horizontalAngle = hAngle;
|
||||
verticalAngle = vAngle;
|
||||
currentDistance = dist;
|
||||
focusPoint = focus;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log("[CameraManager] 드론 모드 해제");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 드론 모드 진입 전 orbit 상태 저장
|
||||
if (currentPreset != null)
|
||||
{
|
||||
float currentFOV = currentPreset.virtualCamera != null ? currentPreset.virtualCamera.Lens.FieldOfView : 60f;
|
||||
currentPreset.SaveOrbitState(targetHorizontalAngle, targetVerticalAngle, targetDistance, targetFocusPoint, currentFOV);
|
||||
}
|
||||
|
||||
droneCameraMode.Activate(currentCamera);
|
||||
Debug.Log("[CameraManager] 드론 모드 진입");
|
||||
}
|
||||
|
||||
// StreamDeck에 상태 변경 알림
|
||||
NotifyStreamDeckCameraStateChanged();
|
||||
}
|
||||
|
||||
public bool IsDroneModeActive => droneCameraMode != null && droneCameraMode.IsActive;
|
||||
|
||||
private void NotifyStreamDeckCameraStateChanged()
|
||||
{
|
||||
if (streamDeckManager != null)
|
||||
{
|
||||
streamDeckManager.NotifyCameraChanged();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Initialization
|
||||
@ -392,6 +519,19 @@ public class CameraManager : MonoBehaviour, IController
|
||||
{
|
||||
inputHandler = gameObject.AddComponent<InputHandler>();
|
||||
}
|
||||
|
||||
// 게임패드 + 드론 모드 초기화
|
||||
gamepadInput = GetComponent<GamepadInputHandler>();
|
||||
if (gamepadInput == null)
|
||||
{
|
||||
gamepadInput = gameObject.AddComponent<GamepadInputHandler>();
|
||||
}
|
||||
|
||||
droneCameraMode = GetComponent<DroneCameraMode>();
|
||||
if (droneCameraMode == null)
|
||||
{
|
||||
droneCameraMode = gameObject.AddComponent<DroneCameraMode>();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeRawInput()
|
||||
@ -556,6 +696,9 @@ public class CameraManager : MonoBehaviour, IController
|
||||
if (!IsValidSetup) return;
|
||||
if (currentCamera == null) return;
|
||||
|
||||
// 드론 모드 활성 시 마우스 제어 스킵
|
||||
if (droneCameraMode != null && droneCameraMode.IsActive) return;
|
||||
|
||||
// 현재 프리셋이 마우스 조작을 허용하지 않으면 스킵
|
||||
if (currentPreset != null && !currentPreset.allowMouseControl) return;
|
||||
|
||||
@ -811,6 +954,26 @@ public class CameraManager : MonoBehaviour, IController
|
||||
var oldPreset = currentPreset;
|
||||
var newCameraName = newPreset.virtualCamera?.gameObject.name ?? "Unknown";
|
||||
|
||||
// 드론 모드 활성 중이면 상태 저장 후 해제
|
||||
if (droneCameraMode != null && droneCameraMode.IsActive && oldPreset != null)
|
||||
{
|
||||
var cam = oldPreset.virtualCamera;
|
||||
savedDroneStates[oldPreset] = new SavedDroneState
|
||||
{
|
||||
wasActive = true,
|
||||
position = cam != null ? cam.transform.position : Vector3.zero,
|
||||
rotation = cam != null ? cam.transform.rotation : Quaternion.identity,
|
||||
fov = cam != null ? cam.Lens.FieldOfView : 60f,
|
||||
target = droneCameraMode.CurrentTarget,
|
||||
targetIndex = -1
|
||||
};
|
||||
droneCameraMode.Deactivate();
|
||||
}
|
||||
else if (droneCameraMode != null && !droneCameraMode.IsActive && oldPreset != null)
|
||||
{
|
||||
// 드론 모드 비활성 상태에서 전환 시 이전 저장 상태 유지 (덮어쓰지 않음)
|
||||
}
|
||||
|
||||
// 이전 프리셋의 orbit 상태 저장
|
||||
if (oldPreset != null && oldPreset.allowMouseControl)
|
||||
{
|
||||
@ -854,6 +1017,27 @@ public class CameraManager : MonoBehaviour, IController
|
||||
|
||||
OnCameraChanged?.Invoke(oldPreset, newPreset);
|
||||
|
||||
// 이전에 저장된 드론 상태가 있으면 복원
|
||||
if (droneCameraMode != null && savedDroneStates.TryGetValue(newPreset, out var droneState) && droneState.wasActive)
|
||||
{
|
||||
var cam = newPreset.virtualCamera;
|
||||
if (cam != null)
|
||||
{
|
||||
cam.transform.position = droneState.position;
|
||||
cam.transform.rotation = droneState.rotation;
|
||||
var lens = cam.Lens;
|
||||
lens.FieldOfView = droneState.fov;
|
||||
cam.Lens = lens;
|
||||
}
|
||||
droneCameraMode.Activate(cam);
|
||||
if (droneState.target != null)
|
||||
{
|
||||
droneCameraMode.ClearTarget();
|
||||
// 타겟 복원은 public 메서드로
|
||||
droneCameraMode.SetTarget(droneState.target);
|
||||
}
|
||||
}
|
||||
|
||||
// 스트림덱에 카메라 변경 알림 전송
|
||||
if (streamDeckManager != null)
|
||||
{
|
||||
@ -1228,7 +1412,8 @@ public class CameraManager : MonoBehaviour, IController
|
||||
current_index = currentIndex,
|
||||
camera_name = currentPreset.virtualCamera?.gameObject.name ?? "Unknown",
|
||||
preset_name = currentPreset.presetName,
|
||||
total_cameras = cameraPresets.Count
|
||||
total_cameras = cameraPresets.Count,
|
||||
is_drone_mode = IsDroneModeActive
|
||||
};
|
||||
}
|
||||
|
||||
@ -1256,6 +1441,7 @@ public class CameraManager : MonoBehaviour, IController
|
||||
public string camera_name;
|
||||
public string preset_name;
|
||||
public int total_cameras;
|
||||
public bool is_drone_mode;
|
||||
}
|
||||
#endregion
|
||||
|
||||
@ -1298,6 +1484,14 @@ public class CameraManager : MonoBehaviour, IController
|
||||
// 카메라 목록은 GetControllerData()에서 반환됨
|
||||
break;
|
||||
|
||||
case "toggle_drone_mode":
|
||||
ToggleDroneMode();
|
||||
break;
|
||||
|
||||
case "get_drone_state":
|
||||
// 드론 상태는 GetCurrentCameraState()의 is_drone_mode로 반환됨
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.LogWarning($"[CameraManager] 알 수 없는 액션: {actionId}");
|
||||
break;
|
||||
|
||||
@ -41,7 +41,7 @@ public class SystemController : MonoBehaviour
|
||||
public bool autoFindFacialMotionClients = true;
|
||||
|
||||
[Tooltip("수동으로 지정할 페이셜 모션 클라이언트 목록 (autoFindFacialMotionClients가 false일 때 사용)")]
|
||||
public List<UnityRecieve_FACEMOTION3D_and_iFacialMocap> facialMotionClients = new List<UnityRecieve_FACEMOTION3D_and_iFacialMocap>();
|
||||
public List<StreamingleFacialReceiver> facialMotionClients = new List<StreamingleFacialReceiver>();
|
||||
|
||||
[Header("Screenshot Settings")]
|
||||
[Tooltip("스크린샷 해상도 (기본: 4K)")]
|
||||
@ -82,12 +82,22 @@ public class SystemController : MonoBehaviour
|
||||
[Tooltip("시작 시 리타게팅 웹 리모컨 자동 시작")]
|
||||
public bool autoStartRetargetingRemote = true;
|
||||
|
||||
[Header("아바타 Head 콜라이더")]
|
||||
[Tooltip("아바타 Head 본에 자동으로 SphereCollider를 생성합니다 (DOF 타겟, 레이캐스트 등에 활용)")]
|
||||
public bool autoCreateHeadColliders = true;
|
||||
|
||||
[Tooltip("Head 콜라이더 반경")]
|
||||
public float headColliderRadius = 0.1f;
|
||||
|
||||
[Header("디버그")]
|
||||
public bool enableDebugLog = true;
|
||||
|
||||
private bool isRecording = false;
|
||||
private Material alphaMaterial;
|
||||
|
||||
// 글로벌 아바타 Head 타겟 목록
|
||||
private List<Transform> avatarHeadTargets = new List<Transform>();
|
||||
|
||||
// 싱글톤 패턴
|
||||
public static SystemController Instance { get; private set; }
|
||||
|
||||
@ -150,6 +160,12 @@ public class SystemController : MonoBehaviour
|
||||
// 리타게팅 웹 리모컨 초기화
|
||||
InitializeRetargetingRemote();
|
||||
|
||||
// 아바타 Head 콜라이더 자동 생성
|
||||
if (autoCreateHeadColliders)
|
||||
{
|
||||
RefreshAvatarHeadColliders();
|
||||
}
|
||||
|
||||
Log("SystemController 초기화 완료");
|
||||
Log($"OptiTrack 클라이언트: {(optitrackClient != null ? "설정됨" : "없음")}");
|
||||
Log($"Motion Recorder 개수: {motionRecorders.Count}");
|
||||
@ -368,7 +384,7 @@ public class SystemController : MonoBehaviour
|
||||
/// </summary>
|
||||
public void RefreshFacialMotionClients()
|
||||
{
|
||||
var allClients = FindObjectsOfType<UnityRecieve_FACEMOTION3D_and_iFacialMocap>();
|
||||
var allClients = FindObjectsOfType<StreamingleFacialReceiver>();
|
||||
facialMotionClients = allClients.ToList();
|
||||
Log($"Facial Motion 클라이언트 {facialMotionClients.Count}개 발견");
|
||||
}
|
||||
@ -799,6 +815,51 @@ public class SystemController : MonoBehaviour
|
||||
|
||||
#endregion
|
||||
|
||||
#region 아바타 Head 콜라이더 관리
|
||||
|
||||
/// <summary>
|
||||
/// 씬의 모든 아바타 Head 본을 찾아 SphereCollider를 생성합니다.
|
||||
/// DOF 타겟, 레이캐스트 등 여러 시스템에서 공통으로 활용됩니다.
|
||||
/// </summary>
|
||||
public void RefreshAvatarHeadColliders()
|
||||
{
|
||||
avatarHeadTargets.Clear();
|
||||
|
||||
var retargetingScripts = FindObjectsByType<CustomRetargetingScript>(FindObjectsSortMode.None);
|
||||
Log($"아바타 Head 콜라이더 검색: CustomRetargetingScript {retargetingScripts.Length}개 발견");
|
||||
|
||||
foreach (var script in retargetingScripts)
|
||||
{
|
||||
if (script.targetAnimator == null) continue;
|
||||
|
||||
Transform headBone = script.targetAnimator.GetBoneTransform(HumanBodyBones.Head);
|
||||
if (headBone == null) continue;
|
||||
|
||||
// 콜라이더가 없으면 추가
|
||||
if (!headBone.TryGetComponent<SphereCollider>(out _))
|
||||
{
|
||||
var collider = headBone.gameObject.AddComponent<SphereCollider>();
|
||||
collider.radius = headColliderRadius;
|
||||
collider.isTrigger = true;
|
||||
Log($"Head 콜라이더 생성: {headBone.name} ({script.targetAnimator.gameObject.name})");
|
||||
}
|
||||
|
||||
avatarHeadTargets.Add(headBone);
|
||||
}
|
||||
|
||||
Log($"총 {avatarHeadTargets.Count}개의 아바타 Head 타겟 등록 완료");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 등록된 아바타 Head 타겟 목록 반환 (다른 시스템에서 활용)
|
||||
/// </summary>
|
||||
public List<Transform> GetAvatarHeadTargets()
|
||||
{
|
||||
return avatarHeadTargets;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region MagicaCloth 시뮬레이션 기능
|
||||
|
||||
#if MAGICACLOTH2
|
||||
@ -1070,6 +1131,11 @@ public class SystemController : MonoBehaviour
|
||||
OpenScreenshotFolder();
|
||||
break;
|
||||
|
||||
// 아바타 Head 콜라이더
|
||||
case "refresh_avatar_head_colliders":
|
||||
RefreshAvatarHeadColliders();
|
||||
break;
|
||||
|
||||
// MagicaCloth 시뮬레이션
|
||||
case "refresh_magica_cloth":
|
||||
case "reset_magica_cloth":
|
||||
|
||||
@ -0,0 +1,178 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
/// <summary>
|
||||
/// 게임패드 입력을 처리하여 드론 카메라 제어에 필요한 값을 제공합니다.
|
||||
/// Unity Input System의 Gamepad.current를 사용합니다.
|
||||
/// </summary>
|
||||
public class GamepadInputHandler : MonoBehaviour
|
||||
{
|
||||
[Header("Deadzone Settings")]
|
||||
[SerializeField] private float stickDeadzone = 0.15f;
|
||||
|
||||
[Header("Response Curve")]
|
||||
[SerializeField] private float stickExponent = 2.0f; // 비선형 응답 곡선 (2 = 제곱)
|
||||
|
||||
[Header("Speed Multipliers")]
|
||||
[SerializeField] private float slowMultiplier = 0.3f; // LT 누를 때
|
||||
[SerializeField] private float boostMultiplier = 3.0f; // RT 누를 때
|
||||
|
||||
// 스틱 값 (데드존 + 커브 적용 후)
|
||||
public Vector2 LeftStick { get; private set; }
|
||||
public Vector2 RightStick { get; private set; }
|
||||
|
||||
// 트리거 값 (0~1)
|
||||
public float LeftTrigger { get; private set; }
|
||||
public float RightTrigger { get; private set; }
|
||||
|
||||
// 속도 배율 (LT/RT 기반)
|
||||
public float SpeedMultiplier { get; private set; } = 1f;
|
||||
|
||||
// 버튼 상태 (이번 프레임에 눌렸는지)
|
||||
public bool ButtonSouthPressed { get; private set; } // A (Xbox) / Cross (PS)
|
||||
public bool ButtonEastPressed { get; private set; } // B (Xbox) / Circle (PS)
|
||||
public bool ButtonNorthPressed { get; private set; } // Y (Xbox) / Triangle (PS)
|
||||
public bool ButtonWestPressed { get; private set; } // X (Xbox) / Square (PS)
|
||||
public bool LeftShoulderPressed { get; private set; } // LB
|
||||
public bool RightShoulderPressed { get; private set; }// RB
|
||||
public bool LeftShoulderHeld { get; private set; }
|
||||
public bool RightShoulderHeld { get; private set; }
|
||||
public bool StartPressed { get; private set; }
|
||||
// D-pad는 소비형 플래그 (LateUpdate에서 읽은 뒤 자동 리셋)
|
||||
private bool _dpadUp, _dpadDown, _dpadLeft, _dpadRight;
|
||||
// D-pad 이전 프레임 상태 (엣지 감지용)
|
||||
private bool _dpadUpHeld, _dpadDownHeld, _dpadLeftHeld, _dpadRightHeld;
|
||||
public bool DpadUpPressed => _dpadUp;
|
||||
public bool DpadDownPressed => _dpadDown;
|
||||
public bool DpadLeftPressed => _dpadLeft;
|
||||
public bool DpadRightPressed => _dpadRight;
|
||||
|
||||
// 게임패드 연결 상태
|
||||
public bool IsGamepadConnected => Gamepad.current != null;
|
||||
|
||||
private bool wasConnected = false;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var gamepad = Gamepad.current;
|
||||
if (gamepad == null)
|
||||
{
|
||||
if (wasConnected)
|
||||
{
|
||||
Debug.Log("[GamepadInputHandler] 게임패드 연결 해제됨");
|
||||
wasConnected = false;
|
||||
}
|
||||
ResetAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!wasConnected)
|
||||
{
|
||||
Debug.Log($"[GamepadInputHandler] 게임패드 연결됨: {gamepad.displayName}");
|
||||
wasConnected = true;
|
||||
}
|
||||
|
||||
// 스틱
|
||||
LeftStick = ApplyDeadzoneAndCurve(gamepad.leftStick.ReadValue());
|
||||
RightStick = ApplyDeadzoneAndCurve(gamepad.rightStick.ReadValue());
|
||||
|
||||
// 트리거
|
||||
LeftTrigger = gamepad.leftTrigger.ReadValue();
|
||||
RightTrigger = gamepad.rightTrigger.ReadValue();
|
||||
|
||||
// 속도 배율 계산
|
||||
SpeedMultiplier = CalculateSpeedMultiplier();
|
||||
|
||||
// 버튼 (이번 프레임 pressed)
|
||||
ButtonSouthPressed = gamepad.buttonSouth.wasPressedThisFrame;
|
||||
ButtonEastPressed = gamepad.buttonEast.wasPressedThisFrame;
|
||||
ButtonNorthPressed = gamepad.buttonNorth.wasPressedThisFrame;
|
||||
ButtonWestPressed = gamepad.buttonWest.wasPressedThisFrame;
|
||||
LeftShoulderPressed = gamepad.leftShoulder.wasPressedThisFrame;
|
||||
RightShoulderPressed = gamepad.rightShoulder.wasPressedThisFrame;
|
||||
LeftShoulderHeld = gamepad.leftShoulder.isPressed;
|
||||
RightShoulderHeld = gamepad.rightShoulder.isPressed;
|
||||
StartPressed = gamepad.startButton.wasPressedThisFrame;
|
||||
// D-pad: ReadValue()로 방향 판별 (일부 게임패드에서 wasPressedThisFrame이 안 먹는 경우 대응)
|
||||
Vector2 dpadValue = gamepad.dpad.ReadValue();
|
||||
bool dpadUpNow = dpadValue.y > 0.5f;
|
||||
bool dpadDownNow = dpadValue.y < -0.5f;
|
||||
bool dpadLeftNow = dpadValue.x < -0.5f;
|
||||
bool dpadRightNow = dpadValue.x > 0.5f;
|
||||
|
||||
// 엣지 감지: 이전 프레임에 안 눌려있다가 이번에 눌린 경우만 플래그 세움
|
||||
if (dpadUpNow && !_dpadUpHeld) _dpadUp = true;
|
||||
if (dpadDownNow && !_dpadDownHeld) _dpadDown = true;
|
||||
if (dpadLeftNow && !_dpadLeftHeld) _dpadLeft = true;
|
||||
if (dpadRightNow && !_dpadRightHeld) _dpadRight = true;
|
||||
|
||||
_dpadUpHeld = dpadUpNow;
|
||||
_dpadDownHeld = dpadDownNow;
|
||||
_dpadLeftHeld = dpadLeftNow;
|
||||
_dpadRightHeld = dpadRightNow;
|
||||
}
|
||||
|
||||
private Vector2 ApplyDeadzoneAndCurve(Vector2 raw)
|
||||
{
|
||||
float magnitude = raw.magnitude;
|
||||
if (magnitude < stickDeadzone)
|
||||
return Vector2.zero;
|
||||
|
||||
// 데드존 이후 0~1로 리매핑
|
||||
float normalized = (magnitude - stickDeadzone) / (1f - stickDeadzone);
|
||||
normalized = Mathf.Clamp01(normalized);
|
||||
|
||||
// 응답 곡선 적용
|
||||
float curved = Mathf.Pow(normalized, stickExponent);
|
||||
|
||||
return raw.normalized * curved;
|
||||
}
|
||||
|
||||
private float CalculateSpeedMultiplier()
|
||||
{
|
||||
// LT와 RT가 동시에 눌리면 기본 속도
|
||||
if (LeftTrigger > 0.1f && RightTrigger > 0.1f)
|
||||
return 1f;
|
||||
|
||||
if (LeftTrigger > 0.1f)
|
||||
return Mathf.Lerp(1f, slowMultiplier, LeftTrigger);
|
||||
|
||||
if (RightTrigger > 0.1f)
|
||||
return Mathf.Lerp(1f, boostMultiplier, RightTrigger);
|
||||
|
||||
return 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// D-pad 플래그를 소비합니다. LateUpdate에서 읽은 후 호출해주세요.
|
||||
/// </summary>
|
||||
public void ConsumeDpad()
|
||||
{
|
||||
_dpadUp = false;
|
||||
_dpadDown = false;
|
||||
_dpadLeft = false;
|
||||
_dpadRight = false;
|
||||
}
|
||||
|
||||
private void ResetAll()
|
||||
{
|
||||
LeftStick = Vector2.zero;
|
||||
RightStick = Vector2.zero;
|
||||
LeftTrigger = 0f;
|
||||
RightTrigger = 0f;
|
||||
SpeedMultiplier = 1f;
|
||||
ButtonSouthPressed = false;
|
||||
ButtonEastPressed = false;
|
||||
ButtonNorthPressed = false;
|
||||
ButtonWestPressed = false;
|
||||
LeftShoulderPressed = false;
|
||||
RightShoulderPressed = false;
|
||||
LeftShoulderHeld = false;
|
||||
RightShoulderHeld = false;
|
||||
StartPressed = false;
|
||||
_dpadUp = false;
|
||||
_dpadDown = false;
|
||||
_dpadLeft = false;
|
||||
_dpadRight = false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5db7d43fd93d24449614ea893fe209d
|
||||
Loading…
x
Reference in New Issue
Block a user