> For the complete documentation index, see [llms.txt](https://docs.decentraland.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.decentraland.org/creator/content-creator-ko/sdk7/interactivity/mouse-movement.md).

# 마우스 이동

플레이어의 마우스 이동에 실시간으로 반응해 드래그 제스처를 구동하고, 오브젝트를 회전시키거나, 사용자 지정 카메라를 조종하세요.

씬은 매 프레임마다 플레이어의 원시 마우스 이동을 읽어 실시간 상호작용을 구동할 수 있습니다. 예를 들어 드래그 및 스와이프 제스처, 플레이어가 끌면 물체를 회전시키거나 미끄러지게 하기, 또는 1인칭 슈터처럼 사용자 지정 카메라를 조작하기 등이 가능합니다.

이를 위해 `screenDelta` 속성을 `PrimaryPointerInfo` 컴포넌트의 `engine.RootEntity`를 읽습니다. 이 속성은 `Vector2` 로, 마지막 프레임 이후 마우스가 몇 픽셀 이동했는지 알려줍니다. 양수 `x` 값은 마우스가 오른쪽으로 이동했음을 의미하고, 양수 `y` 값은 위로 이동했음을 의미하며, 이는 화면 원점이 왼쪽 아래 모서리에 있는 것과 일치합니다. 마우스가 움직이지 않은 프레임에서는 두 값 모두 0입니다.

```ts
import { engine, PrimaryPointerInfo } from '@dcl/sdk/ecs'

engine.addSystem(() => {
	const delta = PrimaryPointerInfo.getOrNull(engine.RootEntity)?.screenDelta
	if (!delta) return
	console.log(`mouse moved: ${delta.x}, ${delta.y}`)
})
```

왜냐하면 `screenDelta` 는 단일 프레임의 이동만 담고 있으므로, 항상 [시스템](/creator/content-creator-ko/sdk7/architecture/systems.md)안에서 읽어야 프레임 사이에 어떤 이동도 놓치지 않습니다.

{% hint style="warning" %}
**📔 참고**: 초기 씬 로딩 시에는 `engine.RootEntity` 을 참조하지 마세요. 엔티티가 아직 초기화되지 않았으면 오류가 발생할 수 있습니다. 이 문제를 피하려면 항상 시스템 안에서 엔티티를 참조하세요. 시스템의 첫 실행은 씬이 이미 올바르게 초기화된 뒤에 호출되므로, 엔티티는 항상 사용할 수 있습니다.
{% endhint %}

{% hint style="warning" %}
**📔 참고**: 이 문서에서 설명하는 내용은 데스크톱 플레이어에게만 해당됩니다.  [모바일 앱](/creator/content-creator-ko/build-for-mobile/mobile-client/overview.md)에서는 입력이 터치 기반이며 자유롭게 움직이는 커서가 없으므로  `screenDelta` 는 항상 0을 보고합니다. 터치 기기에서 입력이 어떻게 동작하는지는 [모바일에서의 입력](/creator/content-creator-ko/build-for-mobile/develop/input-on-mobile.md) 를, 모바일 플레이어에게 대체 조작을 제공하는 방법은 [플랫폼 감지](/creator/content-creator-ko/build-for-mobile/develop/detect-platform.md) 를 참조하세요.
{% endhint %}

## 커서가 잠긴 상태의 마우스 이동

`screenDelta` 는  `PrimaryPointerInfo` 의 다른 속성과 다르게 동작합니다. 커서가 [잠겨 있을 때](/creator/content-creator-ko/sdk7/interactivity/button-events/click-events.md#lock-or-unlock-the-cursor): `screenCoordinates` 는 화면 중앙에 고정되고  `worldRayDirection` 는 항상 화면 중앙의 레이를 보고하지만,  `screenDelta` 는 매 프레임마다 원시 마우스 이동을 계속 보고합니다. 따라서 커서가 잠겨 있을 때 마우스 이동을 읽을 수 있는 유일한 방법이며, 이는 사용자 지정 카메라나 조준 조작에 정확히 필요한 것입니다.

## 드래그하여 물체 회전

다음 예제는 플레이어가 포인터 버튼을 누른 상태에서 마우스를 옆으로 드래그해 큐브를 회전시킬 수 있게 합니다:

```ts
import {
	engine,
	InputAction,
	inputSystem,
	MeshCollider,
	MeshRenderer,
	PrimaryPointerInfo,
	Transform,
} from '@dcl/sdk/ecs'
import { Quaternion, Vector3 } from '@dcl/sdk/math'

const DRAG_SENSITIVITY = 0.5

export function main() {
	const cube = engine.addEntity()
	Transform.create(cube, { position: Vector3.create(8, 1, 8) })
	MeshRenderer.setBox(cube)
	MeshCollider.setBox(cube)

	engine.addSystem(() => {
		// 포인터 버튼이 눌려 있는 동안에만 회전
		if (!inputSystem.isPressed(InputAction.IA_POINTER)) return

		const delta = PrimaryPointerInfo.getOrNull(engine.RootEntity)?.screenDelta
		if (!delta || delta.x === 0) return

		const transform = Transform.getMutable(cube)
		transform.rotation = Quaternion.multiply(
			transform.rotation,
			Quaternion.fromEulerDegrees(0, delta.x * DRAG_SENSITIVITY, 0)
		)
	})
}
```

같은 패턴은 모든 드래그 상호작용에 적용할 수 있습니다. 예를 들어  `delta.x` 를 오프셋으로 사용해 엔티티를 레일을 따라 움직이거나, 애니메이션을 스크러빙하거나, 큰 delta 값으로 빠른 스와이프를 감지할 수 있습니다.

## 마우스룩 카메라 조작

왜냐하면  `screenDelta` 는 커서가 잠겨 있는 동안에도 계속 작동하므로,  [가상 카메라](/creator/content-creator-ko/sdk7/3d/camera.md#using-virtual-cameras)의 마우스룩 조작을 구현하는 데 사용할 수 있습니다. 이를 위해 마우스 이동을 yaw 및 pitch 각도에 누적한 다음, 매 프레임 카메라의 회전에 적용하세요.

다음 예제에서는 플레이어가 상자를 클릭해 마우스로 조작할 수 있는 가상 카메라로 전환하고, 보조 버튼(*F*  또는 오른쪽 클릭)을 눌러 기본 카메라로 돌아갑니다. 가상 카메라가 활성화되어 있는 동안 씬은 커서도 잠그고,  [입력 수정자](/creator/content-creator-ko/sdk7/interactivity/player-avatar.md#freeze-the-player)로 아바타를 고정해, 플레이어가 카메라를 조작하는 동안 눈을 감고 걷지 않도록 합니다.

```ts
import {
	engine,
	Entity,
	InputAction,
	InputModifier,
	inputSystem,
	MainCamera,
	MeshCollider,
	MeshRenderer,
	PointerEventType,
	pointerEventsSystem,
	PointerLock,
	PrimaryPointerInfo,
	Transform,
	VirtualCamera,
} from '@dcl/sdk/ecs'
import { Quaternion, Vector3 } from '@dcl/sdk/math'

// 마우스 이동 픽셀당 카메라 회전 각도(도)
const SENSITIVITY = 0.15

let cameraEntity: Entity
let cameraActive = false
let yaw = 0
let pitch = 0

export function main() {
	// 카메라가 따라가는 엔티티
	cameraEntity = engine.addEntity()
	Transform.create(cameraEntity, { position: Vector3.create(8, 3, 8) })
	VirtualCamera.create(cameraEntity, {
		defaultTransition: { transitionMode: VirtualCamera.Transition.Time(0.5) },
	})

	// 이 상자를 클릭해 마우스룩 모드로 들어갑니다
	const box = engine.addEntity()
	Transform.create(box, { position: Vector3.create(8, 1, 4) })
	MeshRenderer.setBox(box)
	MeshCollider.setBox(box)
	pointerEventsSystem.onPointerDown(
		{
			entity: box,
			opts: { button: InputAction.IA_POINTER, hoverText: '카메라 조작' },
		},
		() => activateCamera(true)
	)

	// 마우스로 카메라를 조작
	engine.addSystem(() => {
		if (!cameraActive) return
		if (!PointerLock.getOrNull(engine.CameraEntity)?.isPointerLocked) return

		const delta = PrimaryPointerInfo.getOrNull(engine.RootEntity)?.screenDelta
		if (!delta) return

		yaw += delta.x * SENSITIVITY
		// 카메라가 뒤집히지 않도록 pitch를 제한
		pitch = Math.max(-85, Math.min(85, pitch - delta.y * SENSITIVITY))

		Transform.getMutable(cameraEntity).rotation = Quaternion.fromEulerDegrees(pitch, yaw, 0)
	})

	// 보조 버튼으로 마우스룩 모드 종료
	engine.addSystem(() => {
		if (!cameraActive) return
		if (inputSystem.isTriggered(InputAction.IA_SECONDARY, PointerEventType.PET_DOWN)) {
			activateCamera(false)
		}
	})
}

function activateCamera(active: boolean) {
	cameraActive = active

	// 가상 카메라를 할당하거나 해제
	MainCamera.createOrReplace(engine.CameraEntity, {
		virtualCameraEntity: active ? cameraEntity : undefined,
	})

	// 카메라를 조작하는 동안 아바타 고정
	InputModifier.createOrReplace(engine.PlayerEntity, {
		mode: InputModifier.Mode.Standard({ disableAll: active }),
	})

	// 커서를 잠가 마우스가 즉시 카메라를 조작하도록 함
	PointerLock.createOrReplace(engine.CameraEntity, { isPointerLocked: active })
}
```

이 예제에서 몇 가지 참고할 점:

* 이 시스템은 `delta.y` 를 pitch에서 빼므로, 마우스를 위로 움직이면 카메라도 위를 향하게 됩니다. 수직 조작을 반전시키고 싶다면 이 부호를 바꾸세요.
* pitch는 -85도에서 85도 사이로 제한되므로, 카메라가 뒤로 뒤집히는 일은 없습니다.
* 그 `SENSITIVITY` 상수는 마우스 이동 픽셀당 회전 각도를 나타내며, 취향에 맞게 조정하세요.

{% hint style="info" %}
**💡 팁**: 플레이어는 언제든지  *Esc* 를 누르거나 오른쪽 클릭하여 커서를 잠금 해제할 수 있으며, 그러면 카메라가 마우스에 반응하지 않게 됩니다. 위 예제의 보조 버튼처럼, 플레이어가 카메라 모드를 완전히 종료할 수 있는 명확한 방법을 항상 제공하세요. 코드로 커서를 다시 잠글 수도 있습니다.  [커서 잠그기 또는 잠금 해제](/creator/content-creator-ko/sdk7/interactivity/button-events/click-events.md#lock-or-unlock-the-cursor).
{% endhint %}

## 관련 주제

커서의 이동이 아니라 화면상의 절대 위치를 읽으려면, `screenCoordinates` 의 동일한 컴포넌트의 속성을 사용하세요.  [플레이어의 커서 위치 확인](/creator/content-creator-ko/sdk7/interactivity/user-data.md#check-the-players-cursor-position).

커서 아래에 있는 엔티티를 알아내려면  `worldRayDirection` 속성을 레이캐스트와 결합하세요.  [레이캐스팅](/creator/content-creator-ko/sdk7/interactivity/raycasting.md).

{% hint style="info" %}
**💡 팁**: 포인터 상태의 작동 예시는  [`0,5-primary-cursor-info`](https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/0,5-primary-cursor-info) 테스트 씬을 보세요. 이 씬은  `PrimaryPointerInfo` 을 매 프레임 읽어  `worldRayDirection` 에 레이캐스트를 수행합니다. [`31,20-pointer-lock-control`](https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/31,20-pointer-lock-control), 이 씬은  `PointerLock.isPointerLocked`; 그리고 [`32,20-virtual-camera-mouse-look`](https://github.com/decentraland/sdk7-test-scenes/tree/main/scenes/32,20-virtual-camera-mouse-look), 이 씬은  `screenDelta` 는 여전히 이동을 보고하는 반면  `screenCoordinates` 은 잠금 중 화면 중앙에 고정된 상태입니다.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.decentraland.org/creator/content-creator-ko/sdk7/interactivity/mouse-movement.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
