Scene Graph предоставляет разнообразные способы создания гибкого и динамичного игрового процесса за счёт широких возможностей взаимодействия между модулями и компонентами. В этом шаблоне демонстрируются несколько способов работы с модулями в среде выполнения (в частности, их создание и удаление), а также то, как можно использовать события компонентов и отправлять запросы для связи между модулями и создания модульных элементов игрового процесса, которые в дальнейшем можно будет использовать повторно.
Создание и удаление готовых элементов
В качестве примера разберём интерактивную зону во втором холле. Когда игрок входит в эту зону, в сцену добавляется готовый элемент (фонарь), который удаляется, когда игрок уходит из неё.
Откройте SpawnPrefabDevice.verse, чтобы проверить, как работает этот код. Это устройство творческого режима, которое использует устройство «Область», чтобы определять, когда игрок в неё входит. Затем она создаёт готовый элемент и удаляет его, когда игрок уходит с триггера. В начале определения класса SpawnPrefabDevice редактируемая область TriggerVolume ссылается на активирующую область.
SpawnPrefabDevice := class(creative_device):
@editable
TriggerVolume:volume_device = volume_device{}Когда игра начинается, устройство определяет готовый элемент (фонарный столб), который будет создан, а также место, где он появится.
OnBegin<override>()<suspends>:void =
PrefabToSpawn:entity = LightPost.P_LightPost{}
SpawnTransform:transform = transform:
Translation := vector3:
Left := -9084.0
Up := -8.0
Forward := -919.0Затем оно получает модуль симуляции и создаёт для работы набор контекстов, допускающих неоднозначность. Сначала в нём используется цикл для повторного выполнения кода, за которым следует выражение race с двумя выражениями block.
if:
SimulationEntity := GetSimulationEntity[]
then:
loop:
race:
block:
TriggerVolume.AgentEntersEvent.Await()
SimulationEntity.AddEntities(array{ PrefabToSpawn })
PrefabToSpawn.SetGlobalTransform(SpawnTransform)
Первое выражение block ожидает, когда агент войдёт в зону устройства «Область». Когда он попадает в неё, готовый элемент (фонарь) добавляется в сцену с помощью функции AddEntities(), после чего перемещается в нужное место с помощью функции SetGlobalTransform(). Второе выражение block ожидает момента, когда агент покинет область, после чего готовый элемент будет удалён из родительского элемента (в данном случае из модуля симуляции).
Поскольку оба выражения block выполняются в цикле выражения race, каждое из них постоянно выполняется, благодаря чему игрок может входить в область и выходить из неё, повторно создавая фонарный столб любое количество раз!
Полный сценарий
Ниже приведён полный код для добавления модуля в сцену и его удаления из неё.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/SceneGraph }
using { /Verse.org/SpatialMath }
using { /UnrealEngine.com/Temporary/Diagnostics }
SpawnPrefabDevice := class(creative_device):
Пересечение модулей
Во втором холле также представлено несколько примеров, демонстрирующих различные способы взаимодействия модулей. К примеру, модули с компонентом сетки могут получить сигнал о наложении на другой модуль или пересечении с ним: для этого используются события EntityEnteredEvent и EntityExitedEvent компонента сетки. Рассмотрим пример с НЛО во втором коридоре и разберём, как работает эта функция.
Когда игра начинается, корова воспроизводит анимацию подъёма, как если бы её похитил НЛО. Корова использует событие EntityEnteredEvent компонента сетки, чтобы определить момент пересечения с НЛО, после чего делает себя невидимой, чтобы это выглядело так, словно её похитили.
Код этого примера находится в файле EntityEnteredExampleComponent.verse. Откройте этот файл в проводнике Verse.
Класс entity_entered_example_component сначала определяет редактируемый массив опорных кадров Keyframes, по которым будет создаваться анимация похищения коровы. Здесь также определяется переменная StartTransform, которая задаёт начальное положение коровы в сцене.
entity_entered_example_component<public> := class<final_super>(component):
@editable
Keyframes:[]keyframed_movement_delta = array{}
var StartTransform:transform = transform{}В функции OnBeginSimulation() сценарий сначала получает fort_round_manager. Это интерфейс для диспетчера раундов Fortnite, который можно использовать для подписки на события начала и окончания раунда. В этом примере сценарии подписывают функцию OnRoundStarted() на начало раунда с помощью функции SubscribeRoundStarted(). Это означает, что функция будет выполняться только в начале игры, а не когда модуль начнёт симуляцию.
OnBeginSimulation<override>():void =
(super:)OnBeginSimulation()
if:
FortRoundManager := Entity.GetFortRoundManager[]
then:
FortRoundManager.SubscribeRoundStarted(OnRoundStarted)Затем в функции OnRoundStarted() сценарий начинается с того, что задаёт StartTransform для глобального преобразования модуля, а затем подписывает событие EntityEnteredEvent компонента сетки на новую функцию OnEntityEntered(), которая будет запускаться всякий раз, когда другая сетка будет накладываться на модуль. Затем создаётся функция PlayCowAbductionAnimation(), чтобы начать поднимать корову.
OnRoundStarted():void =
set StartTransform = Entity.GetGlobalTransform()
if:
Mesh := Entity.GetComponent[mesh_component]
then:
Mesh.EntityEnteredEvent.Subscribe(OnEntityEntered)
spawn { PlayCowAbdcutionAnimation() }Функция PlayCowAbductionAnimation() просто задаёт глобальное преобразование коровы в её начальное преобразование, после чего ожидает некоторое время. Затем она получает сетку и компоненты перемещения по опорным кадрам от модуля, активирует сетку, чтобы сделать корову видимой, а затем задаёт анимацию компонента перемещения по опорным кадрам и воспроизводит её.
PlayCowAbdcutionAnimation()<suspends>:void =
Entity.SetGlobalTransform(StartTransform)
Sleep(2.0)
if:
Mesh := Entity.GetComponent[mesh_component]
MovementComponent := Entity.GetComponent[keyframed_movement_component]
then:
set Mesh.Visible = true
MovementComponent.SetKeyframes(Keyframes, oneshot_keyframed_movement_playback_mode{})
MovementComponent.Play()Функция OnEntityEntered() используется для запуска кода всякий раз, когда корова пересекается с другим модулем, в данном случае с НЛО. Когда это происходит, она снова получает сетку и компоненты перемещения по опорным кадрам, но вместо этого использует их для остановки воспроизведения любой активной анимации и делает корову невидимой, как если бы её похитили. Затем она создаёт новый экземпляр функции PlayCowAbductionAnimation(), чтобы начать процесс заново.
OnEntityEntered(OtherEntity:entity):void =
if:
Mesh := Entity.GetComponent[mesh_component]
MovementComponent := Entity.GetComponent[keyframed_movement_component]
then:
MovementComponent.Stop()
set Mesh.Visible = false
spawn { PlayCowAbdcutionAnimation() }Полный сценарий
Ниже приведён полный сценарий для «похищения» сетки коровы с помощью сетки НЛО.
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/SceneGraph/KeyframedMovement }
using { /Verse.org/SpatialMath }
using { /Fortnite.com/Game }
entity_entered_example_component<public> := class<final_super>(component):
Проверка пересечения
Модули также могут отправлять запросы другим модулям, используя проверки на пересечение. Вместо того, чтобы определять сам момент, когда модуль пересекается с конкретной сеткой или прекращает пересечение с ней, проверку на пересечение можно использовать для получения всех модулей и компонентов, пересекающихся с определённой областью.
Эта область может быть либо самой модулем, заданным областью столкновения, таким как сфера или кубоид, либо точкой, из которой будет производиться симуляция модуля. По такому запросу возвращается список overlap_hit. Каждый overlap_hit предоставляет информацию о компоненте или области, которые пересекаются с исходной областью, и вы можете запросить эти компоненты, чтобы найти связанный с ними модуль.
Например, попробуйте встать на интерактивную зону перед примером FindOverlapHits. При взаимодействии с зоной НЛО создаёт невидимую область коллизии и использует её для отправки запроса и проверки на пересечение. Если область коллизии пересекается с коровой, НЛО похитит её!
Код для этого примера определён в файле FindOverlapHitsExampleComponent.verse. Откройте этот файл в проводнике Verse.
Класс начинает с определения редактируемого устройства области с названием Trigger для создания ссылки на интерактивную область, на которую наступает игрок, а также логической переменной IsAbducting, определяющей, похищается корова или нет. Когда компонент начинает выполнять симуляцию, он подписывает функцию OnRoundStarted() на начало раунда в диспетчере раундов. Аналогичным образом функция OnRoundStarted() подписывает события AgentEntersEvent и AgentExitsEvent в Trigger на функции OnTriggerEntered() и OnTriggerExited() соответственно.
find_overlaphits_example_component<public> := class<final_super>(component):
@editable
Trigger:volume_device = volume_device{}
var IsAbducting:logic = false
OnBeginSimulation<override>():void =
Если игрок заходит в интерактивную зону, а НЛО в этот момент не похищает корову, вызывается функция OnTriggerEntered() и останавливает любое движение НЛО путём получения его компонента keyframed_movement_component и вызова функции Pause(). Затем выполняется вызов функций EnableAbductionBeam() и PerformOverlapCheck(), чтобы включить луч захвата и проверить, находится ли корова прямо под НЛО.
OnTriggerEntered(Agent:agent):void=
if:
not IsAbducting?
MovementComponent := Entity.GetComponent[keyframed_movement_component]
then:
MovementComponent.Pause()
EnableAbductionBeam()
PerformOverlapCheck()Логика проверки пересечения или наложения находится в функции PerformOverlapCheck(). Для симуляции луча захвата эта функция создаёт капсулу с коллизией и задаёт CollisionTransform прямо под НЛО.
PerformOverlapCheck():void =
CollisionCapsule := collision_capsule{Radius := 36.0, Length := 328.0}
var CollisionTransform:transform = Entity.GetGlobalTransform()
set CollisionTransform.Translation.Up = CollisionTransform.Translation.Up - 248.0Затем в выражении for функция вызывает FindOverlapHits(), чтобы найти и вернуть все компоненты и области. Она передаёт CollisionCapsule в качестве области для проверки на столкновение, а CollisionTransform — в качестве места, откуда это столкновение должно симулироваться. Затем берётся каждое пересечение и проверяется, был ли компонент в этом пересечении компонентом сетки, в частности, сеткой SM_Toy_Cow из модуля коровы. Если это так, создаётся функция AbductCow(), передающую корову, которую похитят.
# Perform the overlap check from the entity that contains the mesh_component
for:
Overlap : Entity.FindOverlapHits(CollisionTransform, CollisionCapsule)
# Cast to see if what was overlapped was the Cow
CowMesh := Meshes.SM_Toy_Cow[Overlap.TargetComponent]
CowPrefab := CowMesh.Entity
do:
spawn { AbductCow(CowPrefab) }Чтобы симулировать похищение коровы, модуль создаёт анимацию, а затем воспроизводит её для модуля коровы аналогично тому, как это описано в примере с пересечением модулей выше. Поскольку этот код вызывается из модуля НЛО, а не из модуля коровы, ему необходимо получить компоненты от модуля коровы, а затем передать анимацию, которая будет воспроизводиться. Всё начинается с получения сетки и компонентов перемещения по опорным кадрам от коровы, после чего для IsAbducting устанавливается значение true.
AbductCow(CowEntity:entity)<suspends>:void =
# Get the components on the Cow Prefab
if:
CowMesh := CowEntity.GetComponent[mesh_component]
MovementComponent := CowEntity.GetComponent[keyframed_movement_component]
then:
set IsAbducting = trueПоскольку опорные кадры, используемые в анимации похищения коровы, не задаются в окне «Структура», коду необходимо создать их с учётом различий в положениях НЛО и коровы. Для этого в коде мы получаем разницу в переносе между коровой и НЛО, а затем создаём новую keyframed_movement_delta на основе этих значений. Далее этот опорный кадр задаётся в коде как массив в компоненте перемещения по опорным кадрам, после чего вызывается воспроизведение, чтобы анимировать корову от её начального положения до положения НЛО.
# Get the delta between Cow and UFO
DeltaTransform:transform = transform:
Translation:= Entity.GetGlobalTransform().Translation - CowEntity.GetGlobalTransform().Translation
Scale := vector3{Left:= 0.0, Up:= 0.0, Forward:= 0.0}
# Create a key frame
Keyframe := keyframed_movement_delta:
Transform := DeltaTransform
Duration := 2.0
Easing := ease_in_cubic_bezier_easing_function{}
Корова может двигаться в сторону НЛО при похищении, при этом в коде необходимо сделать так, чтобы она исчезала при пересечении с самим НЛО. Для этого в коде ожидается событие EntityEnteredEvent компонента сетки коровы, а затем вызывается RemoveFromParent() для удаления модуля коровы из сцены. Поскольку корова исчезла, НЛО может снова начать патрулирование, поэтому в коде для НЛО вызывается воспроизведение компонента перемещения по опорным кадрам.
# Wait for Entity Entered Event
CowMesh.EntityEnteredEvent.Await()
# Remove Cow from world
CowEntity.RemoveFromParent()
# Resume UFO Patrol
set IsAbducting = false
if:
UFOMovementComponent := Entity.GetComponent[keyframed_movement_component]
Наконец, функции EnableAdbuctionBeam() и DisableAbductionBeam() выступают в качестве обычных вспомогательных функций, которые включают и выключают сетку луча захвата и компоненты направленного света у НЛО.
EnableAbductionBeam():void =
for:
Mesh : Entity.FindDescendantComponents(Meshes.S_EV_SimpleLightBeam_01)
do:
Mesh.Enable()
for:
Light : Entity.FindDescendantComponents(spot_light_component)
do:
Light.Enable()
Полный сценарий
Ниже приведён полный сценарий для проверки пересечения.
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
using { /Verse.org/SceneGraph/KeyframedMovement }
using { /Verse.org/SpatialMath }
using { /Fortnite.com/Game }
using { /Fortnite.com/Devices }
Последовательная проверка пересечения
Последовательная проверка пересечения — это ещё один важный способ определять пересечения или наложения между модулями. Поиск пересечений — это перемещение объекта на заданное расстояние вдоль определённого вектора. Например, можно создать блок, который двигается по платформе и сталкивает игроков в пропасть, или запустить ракету прямо вперёд, чтобы разрушить стену.
Функция FindSweepHits() возвращает список результатов sweep_hit. Каждый результат sweep_hit предоставляет ту же информацию, что и overlap_hit, например, пересечение с компонентом или областью, а также исходную область или компонент, от которых выполняется поиск пересечений. При этом здесь дополнительно предоставляется информация о положении контакта, нормали, нормали грани и расстоянии вдоль траектории поиска пересечений, на котором и было обнаружено пересечение.
В этом шаблоне используется система последовательной проверки пересечений, которая является более усовершенствованной по сравнению с системой в прошлом примере. Чтобы ознакомиться с этим примером, попробуйте встать на последнюю интерактивную зону во втором холле. Когда вы наступите на эту зону, НЛО создаст луч захвата. Затем он выполнит последовательную проверку на пересечение вниз от сетки НЛО в поисках первого модуля, с которым и будет обнаружено пересечение. Если этим модулем окажется корова, НЛО похитит её как обычно. Однако если корова будет защищена куполом, то при последовательной проверке пересечений первым обнаружится купол, что не позволит НЛО похитить корову.
Откройте FindOverlapHitsExampleComponent.verse в проводнике Verse, чтобы ознакомиться с кодом. Общий подход здесь похож то, что было представлено в примере с пересечением выше: здесь реализована та же логика захвата коровы, а также включения и отключения луча захвата. Основное различие здесь заключается в функции OnTriggerEntered(), которая запускается, когда игрок входит в интерактивную зону перед примером.
Код этой функции запускается аналогично тому, что мы видели в изначальном примере с проверкой пересечения, где выполняется получение компонента перемещения по опорным кадрам из модуля и активация луча захвата.
OnTriggerEntered(Agent:agent):void=
# When a cow is inside the abduction area, stop the ship moving and start the abduction beam.
if:
not IsAbducting?
MovementComponent := Entity.GetComponent[keyframed_movement_component]
then:
MovementComponent.Pause()
EnableAbductionBeam()Однако, поскольку функция использует последовательный поиск пересечений вместо стандартного, логика определения того, находится ли корова под лучом захвата, будет несколько другой. Сначала будет получен первый дочерний элемент модуля НЛО (в данном случае это будет его модуль сетки). Затем создаётся вектор смещения, от которого будет выполняться последовательный поиск пересечений, направленный вниз от НЛО.
# Perform the sweep from the UFO Mesh
if (Child := Entity.GetEntities()[0]):
DisplacementVector := vector3{Left:=0.0, Up:=-300.0, Forward:=0.0}После этого вектор смещения используется для вызова вспомогательной функции FindFirstSweepHit() и передачи в неё сетки НЛО и самого вектора. Если первым компонентом окажется компонент сетки коровы, для её похищения будет создана функция AbductCow().
# Perform the sweep from the UFO Mesh
if (Child := Entity.GetEntities()[0]):
DisplacementVector := vector3{Left:=0.0, Up:=-300.0, Forward:=0.0}
FirstSweepHitEntity := FindFirstSweepHit(Child, DisplacementVector)
# If the First Hit Entity is the Cow Mesh, then abduct the Cow
if (HitEntity := FirstSweepHitEntity?; HitEntity.GetComponent[Meshes.SM_Toy_Cow]):
spawn { AbductCow(HitEntity) }Функция FindFirstSweepHit() принимает модуль, с которым нужно найти пересечение, а также вектор смещения по которому и будет последовательно выполняться поиск. Затем выполняется вызов FindSweepHits() для симуляции поиска пересечения, после чего в выражении `for` выполняется перебор по каждому обнаруженному результату пересечения. Поскольку каждый результат sweep_hit содержит либо компонент, либо область для конкретизации типа, можно целенаправленно запросить либо TargetComponent, либо TargetVolume. В этом случае в коде мы получаем модуль-владелец компонента TargetComponent и возвращаем его как переменную option: `true`, если будет найдено пересечение с компонентом, или же false.
# Returns the first Entity hit by FindSweepHits
FindFirstSweepHit(InEntity:entity, DisplacementVector:vector3):?entity =
for (SweepHit : InEntity.FindSweepHits(DisplacementVector)):
return option{ SweepHit.TargetComponent.Entity }
return falseПолный сценарий
Ниже приведён полный сценарий для последовательной проверки пересечения.
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/Colors }
using { /Verse.org/SceneGraph/KeyframedMovement }
using { /Verse.org/SpatialMath }
using { /Fortnite.com/Game }
using { /Fortnite.com/Devices }