VR 훈련



개요
VR 훈련 샘플은 전체 소스 코드와 함께 제공되며, Fusion을 사용하여 멀티플레이어 VR 훈련 애플리케이션을 만드는 방법을 보여줍니다.
이 샘플의 설계를 위해, 드론을 중심으로 여러 가지 활동(무엇이 무엇인지, 드론 조립, 드론 조종 등)을 구상했습니다.
하지만 이 기반 요소들은 다른 VR 훈련 과정 설계에도 재사용하거나 수정하여 활용할 수 있습니다.
다음은 주요 기능입니다:
- 사용자는 교사 또는 학습자의 역할을 선택할 수 있습니다.
- 학습자는 방에 접속 시 각 활동에 필요한 객체를 개별적으로 받습니다.
- 훈련 활동은 중앙에서 관리되어 참가자 간 동기화됩니다.
- 학습자는 버튼 한 번으로 활동을 재시작할 수 있습니다.
- 의도치 않은 연결 끊김에도 훈련 진행 상황이 유지됩니다.
- 각 학습자의 활동 진행 상황은 실시간으로 동기화됩니다.
- 각 활동 상태에 따라 객체(또는 객체 그룹)를 활성화 또는 비활성화할 수 있습니다.
- 여러 명이 동시에 객체를 조립하거나 조작할 수 있습니다.
- 참가자가 조립을 쉽게 할 수 있도록 시각적 가이드를 제공합니다.
기술적인 관점에서 눈여겨볼 만한 요소는 다음과 같습니다:
- 교차 등록 관리
- 고급 객체 자석 메커니즘
- 여러 플레이어가 동시에 조작 가능한 객체 구조의 생성/해체
- 연결 해제 시, 기존 객체에 대한 권한 복구

기술 정보
- 이 샘플은
Shared Authority
토폴로지를 사용합니다. - Meta Quest용 빌드가 제공됩니다.
- 유니티 2022.3, Fusion 2, Photon Voice 2.53으로 개발되었습니다.
- 2가지 아바타 솔루션을 지원합니다 (자체 제작 단순 아바타 & Ready Player Me 아바타)
시작 전 준비
샘플 실행 방법:
PhotonEngine 관리 화ㄴ에서 Fusion AppId를 생성하고, Fusion 메뉴에서 접근 가능한 Real Time 설정의
App Id Fusion
필드에 붙여 넣습니다.동일한 방식으로 Voice AppId도 생성하여
App Id Voice
필드에 붙여 넣습니다.이후
AvatarSelection
씬을 로드하고Play
버튼을 누릅니다.
다운로드
버전 | 릴리즈 일자 | 다운로드 | |
---|---|---|---|
2.0.5 | Mar 19, 2025 | Fusion VR Training 2.0.5 Build 835 |
APK 다운로드
VR 훈련의 데모 버전을 아래에서 확인할 수 있습니다:

입력 조작 방법
Meta Quest
- 텔레포트: A, B, X, Y 버튼 또는 스틱을 눌러 포인터를 표시합니다. 버튼을 놓으면 허용된 지점으로 텔레포트됩니다.
- 회전: 조이스틱을 좌우로 움직이면 회전합니다.
- 터치: 손을 버튼 위에 올려놓으면 해당 버튼이 전환됩니다.
- 잡기: 객체 위에 손을 올리고 컨트롤러의 잡기 버튼을 누릅니다.
- 드론: 왼쪽 컨트롤러의 메뉴 버튼으로 드론을 켜고 끌 수 있으며, 양쪽 조이스틱으로 조종합니다.
- 펜 색상: 조이스틱을 위 또는 아래로 움직여 펜 색상을 변경합니다.
폴더 구조
/Training
폴더: 이 샘플에 특화된 모든 요소가 포함되어 있습니다./IndustriesComponents
폴더: 다른 산업 샘플들과 공유되는 컴포넌트가 포함되어 있습니다./Photon
폴더: Fusion 및 Photon Voice SDK가 포함되어 있습니다./Photon/FusionAddons
폴더: 이 샘플에서 사용되는 Industries 애드온이 포함되어 있습니다./Photon/FusionAddons/FusionXRShared
폴더: VR 공유 샘플에서 가져온 리그 및 잡기 로직이 포함되어 있으며, 다른 프로젝트와 공유할 수 있는 경량 FusionXRShared SDK를 구성합니다./XR
폴더: 가상 현실 설정 파일이 포함되어 있습니다.
아키텍처 개요
VR 훈련 샘플은 VR 공유 페이지에서 설명된 것과 동일한 코드 베이스를 기반으로 하며, 특히 리그 동기화 기능을 포함하고 있습니다.
여기에서 사용된 잡기 시스템은 VR 공유 - 로컬 리그 잡기 페이지에 설명된 대체 "로컬 리그 잡기" 구현입니다.
이 기본 외에도, 다른 Industries 샘플들과 마찬가지로 이 샘플에는 다음과 같은 재사용 가능한 기능을 다루기 위한 Industries 애드온 확장이 포함되어 있습니다:
- 동기화된 광선
- 이동 유효성 검사
- 터치 기능
- 부드러운 텔레포트 처리
- 시선 추적 시스템 등

훈련 샘플 구성
샘플은 다음 주요 요소를 중심으로 구성됩니다:
- 방에 연결된 참가자. 기본값은 학습자이며, 교사로도 변경할 수 있습니다. 이는
LearningParticipant
클래스로 관리됩니다. - 훈련 매니저(
LearningManager
클래스)는 모든 참가자와 훈련 활동을 전체적으로 관리합니다. - 다양한 훈련 활동들은
LearnerActivityTracker
클래스를 상속받아 구현됩니다. - 학습자의 활동 진행도를 표시하는 점수판이 있습니다.
- 역할(학습자 또는 교사)을 선택하고, 활동 상태를 변경할 수 있는 콘솔 스탠드가 있습니다.
학습자
플레이어가 방에 연결되면, ConnectionManager
게임 오브젝트에 의해 NetworkRigVariant
프리팹이 생성됩니다.
이 프리팹은 다음 기능을 포함한 LearningParticipant
클래스를 포함합니다:
- 예기치 않은 연결 해제를 감지하기 위한
UserID
식별자 관리 LearningManager
에 참가자를 등록 (이 샘플에서는 하나만 존재)- 등록 완료 시 활동 프리팹을 생성하거나, 재연결된 경우 이전 객체에 대한 권한을 복구
참가자 역할
이 구현에서는 참가자가 연결되면 기본적으로 학습자로 간주됩니다(IsLearner
는 생성 시 true로 설정됨).
콘솔의 "교사 역할 요청" 버튼을 클릭하여 역할을 변경할 수 있습니다.
자세한 내용은 콘솔 스탠드 항목을 참조하세요.
등록
사용자가 연결되면, LearningManager
에 등록해야 하며, 그에 따라 방에서 훈련 활동을 위한 슬롯과 요소들이 할당됩니다.
마찬가지로 LearningManager
가 방 연결 시 생성되면, 방 안의 모든 학습자를 찾아 LearningParticipant
의 로컬 캐시를 갱신해야 합니다.
객체(LearningParticipant
와 LearningManager
) 생성 순서가 확실하지 않기 때문에 단순 등록 프로세스에는 다음과 같은 실패 위험이 존재합니다:
LearningManager
가 생성되지 않았거나 유효하지 않은 경우LearningParticipant
가 먼저 생성됨- 반대로
LearningParticipant
가 아직 생성되지 않았을 때LearningManager
가 먼저 생성됨 - 두 객체의 등록 과정이 교차되며 진행되는 경우
따라서 이 복잡한 등록 과정을 처리하기 위해 SubscriberRegistry
애드온을 사용합니다:
LearningParticipant
는LearnerComponent
를 상속하며, 이는Subscriber
애드온 클래스를 상속합니다.LearningManager
는Registry
애드온 클래스를 상속합니다.
자세한 내용은 SubscriberRegistry 항목을 참고하세요.
등록이 완료되면, LearningParticipant
는 훈련 활동에 필요한 객체가 포함된 활동 프리팹을 생성합니다.
이 프리팹은 LearningManager
의 learnerActivitiesPrefab
속성으로 설정되어 있습니다.
LearnerSlotBookedOnLearningManager()
메소드를 참조하세요.
재연결 관리
예기치 않은 연결 해제가 발생해도 훈련 진행 상황이 손실되지 않도록 복구 메커니즘이 구현되어 있습니다.
최초 사용자 연결 시:
- 고유 식별자가 생성되어 네트워크 변수
UserId
에 저장됩니다. - 이 식별자는 로컬 사용자 설정에도 저장됩니다.
UserId
는Recoverable
클래스를 가진 모든 객체에도 저장됩니다.LearningManager
의 상태 권한(State Authority)은 이UserId
를LearnerSlotInfos
네트워크 배열에 할당합니다 (StoreLearner()
메소드).
연결이 끊겼을 경우 이상적인 처리 방식은:
LearningParticipant
는 등록 해제됩니다 (Subscriber
클래스의Despawned()
메소드)LearningManager
는LearnerSlotInfos
배열에서 슬롯을 해제합니다 (CleanLearnerSlotPosition()
)
사용자가 재연결하면:
LearningParticipant
는 로컬 사용자 설정에 저장된UserId
가 존재하는지 확인합니다.- 이
UserId
가LearningManager
의LearnerSlotInfos
배열에 존재하면 재연결된 것으로 간주합니다 (해당 배열은 다른 참가자가 유지하며, 상태 권한을 가짐). - 이 경우, 활동 프리팹은 다시 생성되지 않으며, 참가자는 이전에 자신이 생성했던 객체에 대한 권한을 복구합니다 (
RequestAuthorityOnRecoverablesWithUserId()
호출). - 이를 통해 사용자는 기존 상태로 훈련을 계속 진행할 수 있습니다.
학습 관리자
LearningManager
는 샘플 작동의 중심 요소입니다.
다음 항목들을 관리합니다:
- 학습자 등록
- 학습자 슬롯 할당 (방에는 제한된 수의 학습 테이블 슬롯이 있으며, 각 학습자는 하나의 슬롯에 할당되어야 함)
- 학습 활동 등록
- 학습 활동의 전반적인 상태 관리
- 점수판 및 콘솔 UI 갱신에 필요한 이벤트
LearningManager
는 네트워크 씬 오브젝트입니다.
즉, 모든 클라이언트는 자신만의 사본을 가지지만, 네트워크 데이터는 상태 권한을 가진 클라이언트만 수정할 수 있습니다:
LearnerSlotInfos
: 슬롯 점유 상태를 저장하는 네트워크 배열. 각 항목에는 플레이어의userId
및 연관된NetworkBehaviourId
가 포함됩니다.ActivitiesAvailability
: 활동의 전체 상태(열림 또는 닫힘)를 저장하는 네트워크 딕셔너리
C#
// List to save the slot occupation
[Networked, OnChangedRender(nameof(OnLearnerSlotChange))]
[Capacity(MaxLearner)]
[UnitySerializeField]
public NetworkArray<LearnerSlotInfo> LearnerSlotInfos { get; }
// Collection of Activities Status
[Networked, OnChangedRender(nameof(OnActivitiesAvailability))]
[Capacity(MaxActivities)]
[UnitySerializeField]
public NetworkDictionary<int, ActivityStatus> ActivitiesAvailability { get; }
public struct LearnerSlotInfo : INetworkStruct
{
public NetworkBehaviourId learnerBehaviourId;
public NetworkString<_64> userId;
}
public enum ActivityStatus
{
Open,
Closed
}
학습자 등록 및 슬롯 할당
씬 내 공간이 제한되어 있기 때문에, LearnerSlotInfos
네트워크 배열을 사용하여 슬롯을 할당합니다.
학습자가 LearningManager
에 등록할 때(StoreLearner()
), 이 배열이 갱신됩니다.
변경되면 OnLearnerSlotChange()
메소드가 호출되며, LearningManager
는 학습자에게 활동 프리팹을 생성하도록 요청합니다 (프리팹과 위치는 LearningManager
가 제공합니다).
자세한 내용은 학습자 등록 섹션을 참고하세요.
활동 등록
활동 프리팹은 세 개의 LearnerActivityTracker
를 포함하고 있으며, 각각 하나의 활동을 담당합니다.
프리팹이 생성되면, 활동들은 LearningManager
에 등록됩니다.
각 클라이언트는 로컬 활동 리스트 registeredLearnerActivities
를 갱신합니다 (이 리스트는 ScoreBoardManager
에서 사용됩니다).
해당 활동이 아직 알려지지 않은 경우, LearningManager
의 상태 권한을 가진 클라이언트가 ActivitiesAvailability
네트워크 딕셔너리를 갱신합니다.
자세한 내용은 활동 정보 등록을 참고하세요.
학습 활동
등록
LearningManager
가 모든 LearningParticipant
를 알아야 하는 것처럼(각자에게 슬롯을 할당하기 위해),
LearningManager
는 씬에 존재하는 모든 활동도 인식해야 각 활동을 제어할 수 있습니다 (예: 활동 열기/닫기, 점수판에 알림 전달 등).
이를 위해 LearningManager
와 LearnerActivityTracker
간의 등록 메커니즘이 동일하게 설정되어 있습니다.
사용자가 연결되면, 해당 활동 프리팹이 생성되며 포함된 모든 활동(이 샘플에서는 3개)이 씬에서 LearningManager
를 찾아 등록을 시도합니다.
등록 과정이 완료되면 각 활동은 LearningManager
참조를 보유하고 있으며,
모든 클라이언트의 LearningManager
는 로컬 리스트인 registeredLearnerActivities
에 모든 활동을 기록합니다.
예를 들어, 4명의 학습자가 방에 접속했다면 각 클라이언트에는 다음이 존재합니다:
- 하나의
LearningManager
. 이 객체는 네트워크 씬 오브젝트이며, 하나의 클라이언트만 상태 권한을 가지고 있습니다.
다른 클라이언트들은 네트워크 변수는 수정할 수 없지만, 로컬 변수(학습자 목록, 활동 목록 등)는 유지합니다. registeredLearners
리스트에는 4개의 항목이 있음registeredLearnerActivities
리스트에는 12개의 항목이 있음 (학습자 1명당 3개의 활동)- 씬에는 12개의
LearnerActivityTracker
객체가 존재하며, 각 클라이언트는 자신에게 연결된 3개의 활동에 대해서만 상태 권한을 가짐
학습 활동 정보
모든 활동은 LearnerActivityTracker
를 상속받습니다.
이 클래스에는 다음과 같은 공통 속성이 포함되어 있습니다:
- 활동 상태 (Disabled, ReadyToStart, Started, Succeeded 등)
- 활동 진행률
C#
public enum Status
{
Disabled,
ReadyToStart,
Started,
Paused,
Stopped,
Failed,
Succeeded,
Finished,
PendingSuccessValidation,
CustomStatus1,
CustomStatus2,
CustomStatus3,
CustomStatus4,
CustomStatus5,
}
[Networked, OnChangedRender(nameof(OnActivityStatusOfLearnerChange))]
public Status ActivityStatusOfLearner { get; set; }
[Networked, OnChangedRender(nameof(OnActivityChange))]
public float Progress { get; set; }
ActivityStatusOfLearner
와 Progress
는 모두 네트워크 변수로 설정되어 있어, 다른 참가자가 학습 경로에서 진행 상황을 업데이트할 경우 모든 클라이언트에 알림이 전달됩니다.
학습자가 활동을 완료하는 등의 이유로 ActivityStatusOfLearner
가 업데이트되면, 모든 클라이언트는 알림을 받고 OnActivityStatusOfLearnerChange()
메소드를 실행합니다.
이 함수는 현재 활동 상태에 따라 씬 내 게임 오브젝트를 활성화 또는 비활성화하는 역할을 합니다.
또한 학습자의 행동이 활동의 진행률을 변경하면, 모든 클라이언트에서 OnActivityChange()
함수가 호출됩니다.
그 결과 LearningManager
에서 OnLearnerActivityTrackerChange()
함수가 실행됩니다.
단, ActivityStatusOfLearner
는 상태 권한(State Authority)을 가진 클라이언트만 변경할 수 있다는 점에 유의하세요 (예: 활동의 진행률이 1에 도달했을 때).
C#
public virtual void OnActivityChange()
{
if (Object.HasStateAuthority)
{
CheckActivityProgress();
}
LearningManager?.OnLearnerActivityTrackerChange(this);
}
그런 다음 LearningManager
는 onLearnerActivityTrackerUpdate
이벤트를 전송하며, 이 이벤트는 ScoreBoardManager
와 ControlStandActivityListDisplay
에서 사용됩니다.
활동 1: 무엇이 무엇인가?

이 활동은 특정 드론 부품을 인식하는 것을 포함합니다.
이를 위해 학습자는 각 부품에 해당하는 소켓에 깃발을 배치해야 합니다.
모든 깃발이 올바르게 배치되면 활동 상태는 succeeded
로 변경됩니다.
실패 조건은 이 활동에 구현되어 있지 않지만, 예를 들어 3번의 오류를 범했을 경우 실패로 간주할 수도 있었을 것입니다.
깃발과 소켓 사이의 자석 기능
깃발과 소켓 간 자석 효과는 다음과 같이 구현됩니다:
소켓 측면:
AttractorMagnet
컴포넌트가 "Magnets" 레이어로 설정되어 있어, 근처에 있고 아직 잡히지 않은AttractableMagnet
(깃발)을 끌어당깁니다.
이 메커니즘은 로컬에서만 작동하며, 끌어당기는 측과 끌리는 측 사이에 명시적인 연결 개념은 없습니다.- 자석 요소 간 연결을 생성하고 기억하기 위해
MagnetAttachmentPoint
클래스를 사용합니다(AttachmentPoint
클래스를 상속).
두 객체가 자력으로 붙으면, 그 연결은AttachedPoint
네트워크 변수에 저장되어 네트워크를 통해 동기화되며, 다른 사용자가 해당 객체를 조작할 때 올바르게 처리할 수 있게 됩니다.
여기에는 호환 가능한 연결 지점 간에만 링크가 생성되도록 하는 필터도 포함됩니다. 예를 들어 소켓의Attachment Point Tags
는 "Flag"로 설정되어 있고, 깃발의Compatible Attachment Point Tags
도 "Flag"로 설정되어 있어야 합니다. - 소켓은 또한
NamingTag
클래스를 통해 각 소켓에 어떤 깃발이 기대되는지를 정의합니다.
깃발 측면:
- 깃발에는 두 개의
MagnetAttractable
하위 컴포넌트가 있으며, 하나는 소켓과의 자석 결합용이고 다른 하나는 트레이용입니다.
소켓 결합용 구성:
- 소켓의
AttractorMagnet
에 반응하는AttractableMagnet
이 있습니다 ("Magnets" 레이어 사용) - "Compatible Attachment Point Tags"가 "Flag"로 설정된
MagnetAttachmentPoint
가 존재합니다.
이러한 요소들로 인해 깃발은 소켓과 자석 결합되며, 해당 연결 정보는 네트워크 상에서 동기화됩니다.
자세한 내용은 Structure cohesion add-on - AttachmentPoint 단독 사용법을 참조하세요.
깃발과 트레이 사이의 자석 기능
트레이에 놓인 깃발은 자석 기능의 고급 사용 사례를 보여줍니다.
트레이와 깃발은 구조체를 형성하며, 트레이를 잡으면 전체 구조체가 함께 움직이지만, 깃발을 개별적으로 잡으면 해당 지점에서 구조체가 분리됩니다.
이 아이디어는 다음과 같습니다:
- 깃발이 보조 객체(트레이)에 자력으로 붙을 수 있어야 함 → 각 깃발에는 "Tray" 태그가 설정된
MagnetStructureAttachmentPoint
를 포함한 두 번째MagnetAttractable
하위 컴포넌트가 존재함 - 여러 깃발을 동일한 트레이에 자석으로 부착 가능 → 트레이에는 깃발 수만큼의
AttractorMagnet
및MagnetStructureAttachmentPoint
가 존재함 - 깃발을 트레이 위치로 정확히 이동시키지 않고도 자석 결합이 가능해야 함 → 이를 위해 트레이의
AttractorMagnet
은 "Attract Only On Aligment Axis"로 설정됨 (소켓은 "Match Attracting Magnet Position" 사용) - 트레이를 이동하면 깃발도 함께 따라 움직여야 함 → 이를 위해
StructureCohesion
애드온을 사용함.
깃발과 트레이 모두GrabbableStructurePart
컴포넌트를 가지고 있으며 (StructurePart
를 상속),
소켓과의 결합에 사용되던MagnetAttachmentPoint
는 여기서MagnetStructureAttachmentPoint
로 대체됨.
이렇게 하면AttachmentPoint
로 연결된 객체들은 동일한 구조로 그룹화됨 - 깃발을 잡을 때 구조를 분리하려면 → 깃발의
structuralCohesionMode
는WeightBasedCohesion
으로 설정되며,partWeight
는 낮은 값으로 지정됨 (가벼운 부품 하나만 잡아도 구조가 분리되도록 하기 위함)
자세한 내용은 Structure cohesion add-on - 경량 부품 이동 시 연결 해제를 참고하세요.
깃발
깃발의 전반적인 동작은 각 깃발에 포함된 NamingFlag
클래스에서 관리됩니다.
깃발은 네트워크 상에서 동기화되는 FlagStatus
상태 값을 가집니다.
C#
public enum FlagStatus
{
goodPosition,
badPosition,
notDefined
}
[SerializeField]
[Networked, OnChangedRender(nameof(OnFlagStatusChanged))]
public FlagStatus flagStatus { get; set; } = FlagStatus.notDefined;
NamingFlag
클래스는 AttachmentPoint
를 통해 깃발이 자석 결합되었는지 감지합니다.
C#
void Start()
{
if (attachmentPoint)
{
attachmentPoint.onRegisterAttachment.AddListener(OnSnap);
attachmentPoint.onUnregisterAttachment.AddListener(OnUnSnap);
}
}
OnSnap()
함수는 연결된 지점에 NamingTag
컴포넌트가 포함되어 있는지 확인하고,
그 태그가 깃발에 설정된 기대 태그(namingTag
)와 일치하는지 검사합니다.
일치하면 flagStatus
가 갱신되며, 네트워크에 동기화됩니다.
이 변경은 onFlagStatusChanged
이벤트를 트리거 하며,
모든 클라이언트에서 UpdateFlagRenderer()
메소드를 호출해 시각적 피드백을 업데이트합니다.
진행률
활동의 진행률은 ActivityNaming
클래스(이는 LearnerActivityTracker
클래스를 상속함)에서 관리합니다.
초기화 시, ActivityNaming
클래스는 모든 깃발을 탐색하여 각 깃발의 onFlagStatusChanged
이벤트에 리스너를 추가합니다.
이를 통해 깃발이 소켓에 결합되거나 해제될 때마다 알림을 받고 새로운 진행률을 계산합니다.
그 후, Progress
네트워크 변숫값이 변경되면 LearnerActivityTracker
의 OnActivityChange()
함수가 모든 클라이언트에서 호출됩니다.
결과적으로 LearningManager
에서 OnLearnerActivityTrackerChange()
함수가 실행되고,
이후 scoreBoardManager
는 onLearnerActivityTrackerUpdate
이벤트를 통해 알림을 받습니다.
위치 및 깃발 초기화
활동을 쉽게 재시작할 수 있도록 리셋 버튼이 제공됩니다.
참가자가 버튼을 터치하면, 상위 클래스인 ResetActivity
의 OnReset()
함수가 호출됩니다.
이 부모 클래스는 모든 네트워크 객체를 초기 위치로 복원하고, LearnerActivityTracker
의 상태를 ReadyToStart
로 설정합니다.
하지만 이 활동의 구체적인 초기화 동작은 자식 클래스인 ResetNamingActivity
에서 처리합니다.
예를 들어, RestoreFlagStatus()
는 깃발 상태를 "not defined"로 재설정하고,
RestoreAttachmentPoints()
는 MagnetStructureAttachmentPoint
의 설정을 초기 상태로 되돌립니다.
활동 2: 드론 조립

이 활동의 목표는 드론의 여러 부품을 조립하여 완성하는 것입니다.
학습자에게는 조립 안내도가 제공되며, 드론 부품을 잡으면
그 부품이 어떤 다른 부품들과 조립 가능한지 시각적으로 안내하는 가이드가 표시됩니다.
여러 사람이 동시에 드론을 조작하고 조립할 수 있으며,
모든 부품이 조립되면 활동 상태는 succeeded
로 변경됩니다.
자석 기능 (Magnets)
깃발과 트레이에 사용된 자석과 마찬가지로,
드론의 각 부품도 다음과 같은 자석 관련 컴포넌트를 필요로 합니다:
- 로컬 스냅 효과를 위한
AttractorMagnet
및AttractableMagnet
컴포넌트 - 드론 부품 간 연결을 위한
Attachment Point Tags
및Compatible Attachment Point Tags
가 설정된MagnetStructureAttachmentPoint
- 연결된 부품들이 함께 움직이도록 하기 위한
StructureCohesion
애드온의GrabbableStructurePart
컴포넌트
학습자가 드론 부품의 자석 위치를 쉽게 확인할 수 있도록 MagnetPointVisual
클래스는
자석 요소(예: AttractorMagnet
, AttractableMagnet
, 자력 반경 등)를 시각적으로 표시합니다.
또한 해당 부품이 다른 부품과 자석 결합될 경우, 이 시각적 요소는 자동으로 숨겨집니다.
사용자가 드론을 조립할 때 도움을 주기 위해, StructurePartVisualGuide
클래스는 실시간으로 손에 들고 있는 부품과 호환 가능한 부품들을 분석합니다.
이를 위해 사용자의 부품에 설정된 AttachmentPoint
태그를 structurePartsManager
에 등록된 부품과 비교하고, 다음 조건이 모두 만족되면 연결 라인을 표시합니다:
- 태그가 서로 일치할 것
- 해당 자석이 이미 다른 자석과 연결되어 있지 않을 것
- 가장 가까운 호환 자석일 것
진행률
이 활동의 진행률은 ActivityDroneAssembly
클래스에서 업데이트됩니다 (LearnerActivityTracker
를 상속).
초기화 시, ActivityDroneAssembly
클래스는 모든 MagnetStructureAttachmentPoint
를 탐색합니다.
또한 IAttachmentListener
인터페이스를 구현하고 있어, 자석이 연결되거나 분리될 때마다 알림을 받아 새로운 진행률을 계산합니다.
그 후, Progress
네트워크 변수가 수정되면, 모든 클라이언트에서 LearnerActivityTracker
의 OnActivityChange()
함수가 호출됩니다.
이로 인해 LearningManager
의 OnLearnerActivityTrackerChange()
함수가 실행되며,
scoreBoardManager
는 onLearnerActivityTrackerUpdate
이벤트를 통해 알림을 받습니다.
위치 및 드론 구조 초기화
리셋 버튼을 누르면 활동을 쉽게 다시 시작할 수 있습니다.
참가자가 버튼을 누르면 상위 클래스인 ResetActivity
의 OnReset()
함수가 호출됩니다.
이 부모 클래스는 모든 네트워크 객체를 원래 위치로 재설정하고, LearnerActivityTracker
의 상태를 ReadyToStart
로 설정합니다.
하지만 이 활동에서 수행되어야 할 구체적인 초기화 동작은 자식 클래스인 ResetDroneAssemblyActivity
에서 처리합니다.
RestoreMagnetVisual()
은 드론 부품들의 모든 MagnetPointVisual
을 복원하고,
RestoreStructureAttachment()
는 드론 부품 간의 모든 연결을 삭제합니다.
활동 3: 드론 조종

마지막 활동은 드론을 조종해 목표 지점에 안전하게 착륙시키는 것입니다.
조작 방법
활동이 열린 상태에서는, 각 학습자가 자신의 드론을 Meta Quest 컨트롤러로 조종할 수 있습니다.

드론은 DroneControl
클래스에서 관리됩니다.
드론의 위치와 기울기는 네트워크 트랜스폼(Network Transform)을 통해 동기화되므로,
조이스틱 입력 자체는 동기화할 필요가 없습니다.
또한, 다른 플레이어에게도 적절한 시각 및 사운드 효과를 제공하기 위해,
드론의 상태(UAVStatus
, 예: ReadyToFly, Flying 등)와
DroneRemoteControlStatus
역시 네트워크 상에서 동기화됩니다.
C#
[Networked]
[SerializeField] public UAVStatus DroneStatus { get; set; } = UAVStatus.NotYetInitialized;
[Networked]
[SerializeField] private DroneRemoteControlStatus RemoteControlStatus { get; set; } = DroneRemoteControlStatus.SwitchedOff;
public enum DroneRemoteControlStatus
{
SwitchedOff,
SwitchedOn
}
[System.Serializable]
public enum UAVStatus
{
NotYetInitialized,
ReadyToFly,
LimitedFlyingMode,
Flying,
AutoLanding,
Landed
}
진행률
이 활동의 진행률은 ActivityDroneControl
클래스(이는 LearnerActivityTracker
를 상속)에서 업데이트됩니다.
드론의 위치와 목표 지점 간의 거리를 계산하여 진행률을 측정합니다.
불필요한 네트워크 대역폭 소비를 줄이기 위해, 거리 변화가 ±10% 이상이거나 드론이 목표 지점에 가까워졌을 경우에만 Progress
네트워크 변수가 수정됩니다.
그 후, Progress
네트워크 변수가 수정되면, 모든 클라이언트에서 LearnerActivityTracker
의 OnActivityChange()
함수가 호출됩니다.
이로 인해 LearningManager
의 OnLearnerActivityTrackerChange()
함수가 실행되며,
이후 scoreBoardManager
는 onLearnerActivityTrackerUpdate
이벤트를 통해 알림을 받습니다.
콘솔 스탠드
콘솔 스탠드는 다음의 용도로 사용됩니다:
- 참가자 역할(교사 또는 학습자) 선택
- 활동 상태(열림 또는 닫힘) 보기 및 변경

참가자 역할 선택
방에 연결된 참가자는 학습자(기본값) 또는 교사가 될 수 있습니다.
콘솔에서 역할을 변경하는 버튼에 대한 사용자의 상호작용은 해당 사용자의 클라이언트에서만 감지됩니다.
버튼의 표시 여부는 ControlStandUserRoleButtonManager
에서 관리합니다.
버튼을 누르면 PlayerRoleSelection
클래스의 RequestTrainerRole()
또는 RequestLearnerRole()
메소드가 호출되어 IsLearner
불리언 값을 변경합니다.
이 값은 네트워크로 동기화되므로 모든 클라이언트가 알림을 받아 사용자의 모자(Material)를 업데이트할 수 있습니다.
C#
[Networked, OnChangedRender(nameof(OnIsLearnerChange))]
public NetworkBool IsLearner { get; set; }
void OnIsLearnerChange()
{
UpdateLearnerCap();
}
참가자가 다시 학습자 역할을 요청하면, 기존에 할당된 슬롯과 관련 객체는 해제되어 다른 학습자를 위한 공간이 확보됩니다.
활동 상태 관리
콘솔 스탠드는 활동 상태를 표시하고 변경하는 데도 사용됩니다.
ControlStandActivityListDisplay
는 LearningManager
의 onActivityListUpdate
및 onActivityUpdate
이벤트를 수신하여 UI를 업데이트합니다.
C#
private void Start()
{
...
learningManager.onActivityListUpdate.AddListener(UpdateUIForActivities);
learningManager.onLearnerActivityTrackerUpdate.AddListener(UpdateUIForActivityUpdate);
}
onActivityListUpdate
이벤트는 LearningManager
의 ActivitiesAvailability
네트워크 딕셔너리가 갱신될 때(예: 새 활동이 등록되거나 삭제되었을 때, 상태가 변경되었을 때) 발생합니다.
참가자가 활동 상태 변경 버튼을 누르면 learningManager.ToggleActivityStatus(currentIndex)
가 실행되어 새로운 상태가 ActivitiesAvailability
에 설정됩니다.
이후 ActivityListUpdateEvent()
가 호출되며, onActivityListUpdate
이벤트가 트리거 됩니다.
C#
public void OnActivitiesAvailability(NetworkBehaviourBuffer previousBuffer)
{
ActivityListUpdateEvent();
InformLearnerTrackerInfo(previousBuffer);
}
private void ActivityListUpdateEvent()
{
if (onActivityListUpdate != null)
{
onActivityListUpdate.Invoke(this);
}
}
ActivityListUpdateEvent()
외에도, 참가자가 버튼을 터치하면 InformLearnerTrackerInfo()
메소드가 호출됩니다.
이 메소드는 어떤 값(활동 ID)이 업데이트되었는지 확인한 뒤, 해당 ID를 가진 모든 등록된 활동을 갱신하기 위해 OnLearningManagerActivityUpdate(activityId)
를 호출합니다.
그다음, OnLearningManagerActivityUpdate()
함수 내부에서는 상태 권한을 가진 클라이언트가 네트워크 변수인 ActivityStatusOfLearner
를 변경합니다.
이에 따라 모든 클라이언트는 마지막 상태를 기준으로 활성화된 오브젝트들을 업데이트합니다 (UpdateActivatedObjectsBasedOnStatus()
호출).
점수판
ScoreBoardManager
는 점수판을 다음과 같은 상황에서 업데이트하는 역할을 합니다:
- 학습자가 방에 입장하거나 퇴장할 때
- 학습자의 활동 진행률이 변경되었을 때
이를 위해 ScoreBoardManager
는 LearningManager
의 이벤트들을 구독합니다:
C#
private void Start()
{
...
learningManager.onNewLearner.AddListener(OnNewLearner);
learningManager.onDeletedLearner.AddListener(OnDeletedLearner);
learningManager.onActivityUpdate.AddListener(UpdateUIForActivity);
}
이벤트 및 변경 사항에 따라 ScoreBoardManager
는 다음을 수행합니다:
- 점수판에 행을 추가 또는 삭제
- 활동 진행률 막대 업데이트
사용된 XR 애드온 및 Industries 애드온
3D/XR 프로젝트 프로토타입을 보다 쉽게 시작할 수 있도록 몇 가지 무료 애드온을 제공합니다.
자세한 내용은 XR 애드온을 참조하세요.
또한 Industries Circle 회원에게는 재사용 가능한 애드온의 포괄적인 목록을 제공합니다.
자세한 내용은 Industries 애드온을 참조하세요.
이번 샘플에서 사용된 애드온 목록은 아래와 같습니다:
XRShared
XRShared 애드온은 Fusion과 호환되는 XR 경험을 만들기 위한 기본 컴포넌트를 제공합니다.
플레이어 리그의 동기화를 담당하며, 잡기 및 텔레포트와 같은 간단한 기능을 제공합니다.
XRShared 참조.
Connection Manager
ConnectionManager
애드온을 사용하여 사용자 연결을 관리하고, 사용자 아바타를 생성합니다.
Subscriber Registry
SubscriberRegistry
애드온을 사용하여 LearningManager
와 LearningParticipant
/ LearnerActivityTracker
클래스 간의 등록 절차를 처리합니다.
Reconnection
사용자의 연결 해제 및 재연결을 처리하고, 이 이벤트를 감지하며,
씬에 남아 있는 Recoverable
객체에 대한 권한을 복구하기 위해 Reconnection
애드온을 사용합니다.
Reconnection 애드온 참조.
Magnets
이 애드온은 객체들을 서로 스냅(자석처럼 붙임)하는 데 사용됩니다.
Magnet 애드온 참조.
Structure Cohesion
Structure Cohesion 애드온은 여러 객체를 하나의 구조로 결합하여,
사용자가 한 객체를 움직이면 연결된 모든 객체가 함께 움직이도록 합니다.
로코모션 검증
플레이어가 방을 벗어나거나 가구에 부딪히는 것을 방지하기 위해
플레이어의 이동을 제한하는 locomotion validation
애드온을 사용합니다.
다이내믹 오디오 그룹
사용자들이 서로 대화할 수 있도록 Dynamic Audio Group
애드온을 사용합니다.
이 애드온은 사용자 간의 거리를 고려하여 대화 품질을 최적화하고, 대역폭 사용을 최소화합니다.
자세한 내용은 Dynamic Audio group Industries Addons를 참조하세요.
드로잉
방 안에는 2D 펜이 장착된 화이트보드가 포함되어 있습니다.
사용자가 드로잉을 마치고(예: "트리거" 버튼에서 손을 떼면) 핸들이 표시되어 2D 이동이 가능해집니다.
자세한 내용은 3D & 2D 드로잉 Industries Addons를 참조하세요.
데이터 동기화 헬퍼
이 애드온은 이 샘플에서 2D 드로잉 포인트를 동기화하는 데 사용됩니다.
자세한 내용은 Data Sync Helpers Industries 애드온을 참조하세요.
접촉 차단
이 애드온은 화이트보드 표면에서 2D 펜 및 드로잉 핀의 접촉을 차단하는 데 사용됩니다.
자세한 내용은 Blocking contact Industries 애드온을 참조하세요.
피드백
애플리케이션에서 사용되는 사운드를 중앙에서 관리하고, 햅틱 및 오디오 피드백을 처리하기 위해 Feedback
애드온을 사용합니다.
자세한 내용은 피드백 애드온을 참조하세요.
서드파티 에셋 및 저작권 고지 (3rd Party Assets and Attributions)
Fusion VR 훈련 샘플은 다음과 같은 훌륭한 서드파티 에셋을 기반으로 구축되었습니다:
- Oculus Integration
- Oculus Lipsync
- Oculus 샘플 프레임워크의 손 모델
- 사운드
- nathangibson - Universal UI Soundpack
- obsydianx - interface-sfx-pack-1
- Pixabay
- Jingle_Win_Synth_03.wav by LittleRobotSoundFactory 라이선스: Attribution 4.0
- Applause by Kubuzz 라이선스: Creative Commons 0
- drone DJI.wav by bruno.auzet 라이선스: Creative Commons 0
- BEEP - 1 by SamuelGremaud 라이선스: Creative Commons 0
- 3D 모델