-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathshared.verse
1818 lines (1450 loc) · 81.5 KB
/
shared.verse
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This file contains several classes and extension methods used in a Fortnite creative project.
# The classes include:
# - cage_swap_device: a device that handles all players joining a game.
# - shop_device: a device that handles a shop where players can purchase items.
# - shop_item: a class that represents an item in the shop.
# - shared_waiter: a generic class that can wait on multiple events and return the first event that occurs.
# - mutatorDevice: a device that freezes player actions while a cinematic sequence is playing.
# The extension methods include:
# - ToString: an extension method for the logic type that converts it to a string.
# - ToMessage: an extension method for the string type that converts it to a message.
# - MakeMessageFromString: a method that creates a message from a string.
# Each class and method is documented in more detail within its own code block.
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Fortnite.com/Characters }
using { /Fortnite.com/Game }
using { /Verse.org/Concurrency }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Verse.org/Random }
using { /Fortnite.com/AI }
using { /Fortnite.com/FortPlayerUtilities }
using { /Verse.org/Simulation/Tags }
fort_callback := type {_(:fort_character):void}
# from youtube warforge live broadcast, put it within a class
# HotDogPickedUpEvent : fort_callback
# logger that can used in <transacts> and <decides> functions
my_log_channel<public> := class(log_channel):
tagSpawner := class(tag){}
# A project-wide "Logger" to print messages from functions that are not in a class with a log.
# The non-Logger Print is <no_rollback>, so it can't be used in a <transacts> function.
ProjectLog<public>(Message:[]char, ?Level:log_level = log_level.Normal)<transacts>:void=
Logger := log{Channel := my_log_channel}
Logger.Print(Message, ?Level := Level)
# Usage:
# ProjectLog("EndGame: Player has won!", ?Level := log_level.Verbose)
# You don't have to specify the Level
# ProjectLog("Hello Verse!")
# interesting about how to handle all players joining.
cage_swap_device := class(creative_device):
OnBegin<override>()<suspends>:void=
AllPlayers := GetPlayspace().GetPlayers()
for(Player : AllPlayers):
AgentSetup(Player)
GetPlayspace().PlayerAddedEvent().Subscribe(OnJoin)
AgentSetup(Agent: agent):void= {}
OnJoin(Agent: agent):void=
AgentSetup(Agent)
# extension methods
(Value : logic).ToString<public>() : string =
if (Value?):
return "true"
return "false"
# (Value: string).ToMessage<localizes>() : message = "{Value}"
(Translation:vector3).ToString<public>():string =
return "V3 ~ X: {Translation.X}, Y: {Translation.Y}, Z: {Translation.Z}"
(Rotation:rotation).ToString<public>():string =
return "Rot2V3: {Rotation.GetAxis()}"
(Transform:transform).ToString<public>():string =
return "Transform ~ Translation: {Transform.Translation.ToString()}, Rotation: {Transform.Rotation.ToString()}, Scale: {Transform.Scale.ToString()}"
MakeMessageFromString<localizes>(Text: string): message = "{Text}"
# Removes an element from the given map and returns a new map without that element
RemoveKeyFromMap(ExampleMap:[string]int, ElementToRemove:string):[string]int=
var NewMap:[string]int = map{}
# Concatenate Keys from ExampleMap into NewMap, excluding ElementToRemove
for (Key -> Value : ExampleMap, Key <> ElementToRemove):
set NewMap = ConcatenateMaps(NewMap, map{Key => Value})
return NewMap
# Returns Number converted to a string, truncated to Decimals places.
ToTruncatedString<public>(Number:float, Decimals:int):[]char=
var Str:[]char = ToString(Number)
if:
DotIndex := Str.Find['.']
StopIndex := if (Decimals > 0) then Min(DotIndex+Decimals+1,Str.Length) else DotIndex
Tmp := Str.Slice[0, StopIndex]
set Str = Tmp
Str
# Device Configuration:
# 1. Set the 'Cashier' Conditional Button Device's Key Item to use the currency type you want (Gold, etc).
# 2. Set the HUD Message Device to 'Message Recipient: Triggering Player'.
# 3. Set the Button Device to the desired Interact Time.
# 4. Add the Item being purchased to the Item List of the Item Granter.
shop_device := class(creative_device):
@editable
Cashier: conditional_button_device = conditional_button_device{}
@editable
HUDMessage: hud_message_device = hud_message_device{}
@editable
ShopItems: []shop_item = array{}
MakeMessage<localizes>(Text: string): message = "{Text}"
OnBegin<override>()<suspends>:void=
ShopSetup()
ShopSetup(): void =
for (Item : ShopItems):
Item.Init(Self)
ShowHUDMessage(Agent: agent, Text: string): void =
HUDMessage.SetText(MakeMessage(Text))
HUDMessage.Show(Agent)
shop_item := class<concrete>():
var VerseDevice: shop_device = shop_device{}
@editable
ItemName: string = "Item"
@editable
ItemCost: type{X: int where X >= 0} = 10
@editable
Button: button_device = button_device{}
@editable
ItemGranter: item_granter_device = item_granter_device{}
MakeMessage<localizes>(Text: string): message = "{Text}"
Init(MainDevice: shop_device): void =
set VerseDevice = MainDevice
Button.InteractedWithEvent.Subscribe(PurchaseAttempt)
if (ItemCost > 0):
Button.SetInteractionText(MakeMessage("Purchase {ItemName}"))
else:
Button.SetInteractionText(MakeMessage("Pick Up {ItemName}"))
PurchaseAttempt(Agent: agent): void =
if (ItemCost > 0):
PlayerGold := VerseDevice.Cashier.GetItemCount(Agent, 0)
if (PlayerGold >= ItemCost):
VerseDevice.Cashier.SetItemCountRequired(0, ItemCost)
VerseDevice.Cashier.Activate(Agent)
ItemGranter.GrantItem(Agent)
VerseDevice.ShowHUDMessage(Agent, "Purchased {ItemName}.")
else:
VerseDevice.ShowHUDMessage(Agent, "Not enough resources.")
else:
ItemGranter.GrantItem(Agent)
VerseDevice.ShowHUDMessage(Agent, "Obtained {ItemName}.")
# Generic class which you can set up to wait on multiple events
# and return the event that does occur and its payload.
# When you instantiate the class, you define the payload type you want. For example:
# SharedWaiter:shared_waiter(int) = shared_waiter(int){}
shared_waiter<public>(payload:type) := class(awaitable(payload)):
# Single custom event to wait and signal on
SharedEvent<internal>:event(payload) = event(payload){}
# Wait on the custom shared event. Result is the associated payload.
Await<override>()<suspends>:payload=
SharedEvent.Await()
# Add the event to wait on and the payload associated with the event if it occurs
AddAwaitable<public>(Awaitable:awaitable(t), Payload:payload where t:type):void=
spawn{AwaitFirstSignal(Awaitable, Payload)}
# Race between the multiple events. First event to occur signals the shared event
# and cancels the wait for any remaining events that are still waiting to occur.
AwaitFirstSignal<internal>(Awaitable:awaitable(t), Payload:payload where t:type)<suspends>:void=
race:
SharedEvent.Await()
block:
Awaitable.Await()
SharedEvent.Signal(Payload)
# 在播sequencer时冻结玩家操作
mutatorDevice := class(creative_device):
@editable SequencerDevice:cinematic_sequence_device = cinematic_sequence_device{}
PlaySequence()<suspends>:void=
# Put every player in stasis
for (Player : GetPlayspace().GetPlayers()):
if (Character := Player.GetFortCharacter[]):
Character.PutInStasis(stasis_args{})
# Play the sequence
SequencerDevice.Play()
# Wait for the sequence to end
SequencerDevice.StoppedEvent.Await()
# Release every player from stasis
for (Player : GetPlayspace().GetPlayers()):
if (Character := Player.GetFortCharacter[]):
Character.ReleaseFromStasis()
queue<public>(t:type) := class:
Elements<internal>:[]t = array{}
Enqueue<public>(NewElement:t):queue(t)=
queue(t){Elements := Elements + array{NewElement}}
Dequeue<public>()<decides><transacts>:tuple(queue(t),t)=
FirstElement := Front[]
(queue(t){Elements := Elements.RemoveElement[0]}, FirstElement)
Size<public>()<transacts>:int=
Elements.Length
IsEmpty<public>()<decides><transacts>:void=
Size() = 0
Front<public>()<decides><transacts>:t=
Elements[0]
Rear<public>()<decides><transacts>:t=
Elements[Elements.Length - 1]
CreateQueue<public><constructor>(InitialElements:[]t where t:type) := queue(t):
Elements := InitialElements
# This snippet sends a guard to a prop and then keeps the prop near the guard as they move to their next destination, creating the appearance that the guard has picked up and moved the prop.
guard_prop_carry := class(creative_device):
# A reference to the Guard Spawner device. This must be set in the Details panel of this Verse device before starting the game.
@editable
GuardSpawner:guard_spawner_device = guard_spawner_device{}
# A reference to prop that will spawn when the game starts. This must be set in the Details panel of Verse device before starting the game.
@editable
Loot:creative_prop_asset = DefaultCreativePropAsset
# A reference to the guard that will spawn when the game starts
var MaybeGuardFollower:?agent = false
# A reference to the result of calling SpawnProp()
# The ?creative_prop value needs to be false so we can check if the prop did spawn.
# The spawn_prop_result will be overwritten so it can be set to anything.
var PropResult:tuple(?creative_prop, spawn_prop_result) = (false, spawn_prop_result.Ok)
# Runs when the device is started in a running game
OnBegin<override>()<suspends>:void=
# Spawn the prop to be navigated to
# This location was chosen at random
set PropResult = SpawnProp(Loot, vector3{X := 832.000000,Y := 1168.000000, Z := 0.000000}, rotation{})
# Await the Guard Spawner SpawnedEvent
GuardAgent := GuardSpawner.SpawnedEvent.Await()
# OnGuardSpawned(GuardAgent)
# Short Sleep to ensure you can see the guard start the navigation.
# This is not required for the code to function.
Sleep(2.0)
# Create a Task so we know when NavigateToProp completes
NavigateTask := spawn{NavigateToProp()}
# Await NavigateToProp completion
NavigateTask.Await()
# When we know the guard has reached the prop, make the prop follow the guard
AttachPropToGuard()
# Send guard to a location near the prop
NavigateToProp()<suspends>:void=
if:
GuardFollower := MaybeGuardFollower?
Prop := PropResult(0)?
FollowerNav := GuardFollower.GetFortCharacter[].GetNavigatable[]
then:
Print("GuardFollower navigating to prop")
PropLocation := Prop.GetTransform().Translation
# Here we use a random offset distance from the prop so the guard does not jump on top of the prop or try to destroy it
RandomOffsetFromProp := vector3{X:=PropLocation.X + GetRandomFloat(-200.0, 200.0), Y:= PropLocation.Y + GetRandomFloat(-200.0, 200.0), Z:= PropLocation.Z}
NavigationTarget := MakeNavigationTarget(RandomOffsetFromProp)
FollowerNav.NavigateTo(NavigationTarget)
# Make the prop "follow" the guard by looping a call to MoveTo that moves the prop to the guard's Transform
AttachPropToGuard()<suspends>:void=
if:
Prop := PropResult(0)?
GuardFollower := MaybeGuardFollower?
GuardFortCharacter := GuardFollower.GetFortCharacter[]
then:
loop:
Prop.MoveTo(GuardFortCharacter.GetTransform(), 1.0)
# 注意index的使用,这里的index是从0开始的,所接的符号不是:, but :=
ForLoopWithIndex():void =
Arr := array{1, 2.0, "a"}
for (Index := 0..Arr.Length-1):
if(Value := Arr[Index]) {Print("Index: {Index}, Value: unknown type so skip to string...")}
StandStillEvents := module:
# Copyright (c) 2023 Rift9
# Licensed under the MIT License. See https://opensource.org/licenses/MIT for details.
using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/SpatialMath }
# This maximum amount of distance travelled between an update which will trigger a stand still event
StandStillMovePerSecondThreshold: float = 0.01
# Update 3 times per second, enough to detect standing still
PlayerStandStillUpdateFrequency: float = 0.333
# This data struct contains everything needed for tracking a given player
player_standstill_tracker := class():
# Keep a reference to the agent of this player
Agent: agent
# Keep a reference to the agent of this player
Character: fort_character
# Fires when player stands still
StandStillEvent: event() = event(){}
# Fires when player stops standing still
StopStandingStillEvent: event() = event(){}
# Player transform this simulation update
var CurrentTranslation: vector3 = vector3{}
# Toggles when player stand-stills are detected
var PlayerIsStandingStill<private>: logic = true
Update(ElapsedSeconds: float): void =
# No divide by Zeros allowed
if. ElapsedSeconds.IsAlmostZero[0.000001] then. return
PlayerTransform := Character.GetTransform()
PlayerMoveDistanceSquared := DistanceSquared(CurrentTranslation, PlayerTransform.Translation)
# Set new data in the map
set CurrentTranslation = PlayerTransform.Translation
MovementPerSecond := PlayerMoveDistanceSquared / Pow(ElapsedSeconds, 2.0)
# Check for stand still event
if. PlayerIsStandingStill = false and MovementPerSecond < Pow(StandStillMovePerSecondThreshold, 2.0)
then:
set PlayerIsStandingStill = true
StandStillEvent.Signal()
# Else check for start move again event
else if. PlayerIsStandingStill = true and MovementPerSecond > Pow(StandStillMovePerSecondThreshold, 2.0)
then:
set PlayerIsStandingStill = false
StopStandingStillEvent.Signal()
# Get tracking data for a given agent
IsStandingStill()<transacts><decides>: void =
PlayerIsStandingStill = true
# Create a logging class to get more options for `Print` and a channel name automatically added to the log
standstill_tracking_device_log<internal> := class(log_channel){}
# This class handles iterating all players in the game and tracking properties
# Keeping this tracking centralized prevents multiple devices doing iteratiosn over player list
standstill_tracking_device := class(creative_device):
Logger<private> : log = log{Channel:=standstill_tracking_device_log}
# A map of player data used for tracking values between simulation updates
var Players<private>: [agent]player_standstill_tracker = map{}
var TimeLastUpdate<private>: float = 0.0
OnBegin<override>()<suspends>: void =
set TimeLastUpdate = GetSimulationElapsedTime()
# Use playspace to find players
Playspace := GetPlayspace()
PlayspacePlayers := Playspace.GetPlayers()
# Initialize existing players
for:
Player : PlayspacePlayers
FortCharacter := Player.GetFortCharacter[]
PlayerAgent := FortCharacter.GetAgent[]
do. InitAgent(PlayerAgent)
# Subscribe to future players leaving and joining
Playspace.PlayerAddedEvent().Subscribe(HandlePlayerAdded)
Playspace.PlayerRemovedEvent().Subscribe(HandlePlayerRemoved)
# Loops indefinitely
loop:
# We need to calculate time adjusted move amount
ElapsedSeconds := GetSimulationElapsedTime() - TimeLastUpdate
set TimeLastUpdate = GetSimulationElapsedTime()
# Keep track of all player translations
for. PlayerData : Players do. PlayerData.Update(ElapsedSeconds)
# No syncronous infinite loop!
Sleep(PlayerStandStillUpdateFrequency)
# Get tracking data for a given agent
GetAgentTrackingData(Agent: agent)<transacts><decides>: player_standstill_tracker =
Players[Agent]
# Playspace event
HandlePlayerAdded<private>(Player : player) : void =
if. PlayerAgent := Player.GetFortCharacter[].GetAgent[]
then. InitAgent(PlayerAgent)
# Playspace event
HandlePlayerRemoved<private>(Player : player) : void =
if. PlayerAgent := Player.GetFortCharacter[].GetAgent[]
then. ClearAgent(PlayerAgent)
# Create player data
InitAgent<private>(Agent: agent): void =
if:
Player := player[Agent]
PlayerCharacter := Player.GetFortCharacter[]
PlayerTransform := PlayerCharacter.GetTransform()
set Players[Agent] = player_standstill_tracker{
Agent:=Agent
Character:=PlayerCharacter
CurrentTranslation:=PlayerTransform.Translation
}
else. Logger.Print("Error intializing player data", ?Level:=log_level.Error)
# Remove player data
ClearAgent<private>(Agent: agent): void =
set Players = RemoveAgentFromMap(Players, Agent)
# Map edit function. Would be nice if it was generic
RemoveAgentFromMap<private>(Map:[agent]player_standstill_tracker, ElementToRemove: agent): [agent]player_standstill_tracker =
var NewMap: [agent]player_standstill_tracker = map{}
for:
Key -> Value : Map
Key <> ElementToRemove
do. set NewMap = ConcatenateMaps(NewMap, map{Key => Value})
return NewMap
BasicRadioRepeater := module:
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Random }
# radio repeater requires more than one radio device to work
# set device for four second intro/outro or update setting
# stock code plays radio for three minutes per track
radio_repeater := class(creative_device):
var Index:int = 0
@editable
FadeTime:float = 4.0
@editable
TrackTime:float = 180.0
@editable
Radios:[]radio_device = array{}
OnBegin<override>()<suspends>:void=
if( Radios.Length > 0 ):
loop:
if:
PrevRadio := Radios[ Index ]
then:
set Index = GetRandomInt( 0 , Radios.Length - 1 )
if( NextRadio := Radios[ Index ] ):
PrevRadio.Stop()
Sleep( FadeTime )
NextRadio.Play()
Sleep( TrackTime )
UiDialogModule := module:
using { /Fortnite.com/Devices }
using { /Fortnite.com/UI }
using { /Verse.org/Simulation }
using { /Verse.org/Verse}
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /UnrealEngine.com/Temporary/UI }
using { /UnrealEngine.com/Temporary/SpatialMath }
using { /Verse.org/Colors }
<#
What this Verse Device does:
- In property fields in device Details, dev can specify text strings for the UI dialog “Title”, arrays(lists) for Line 1 and Line 2 text, and text for the “Next” and “Exit” UI Buttons. Optional Item Granter and SFX.
- Button Device interacted with ==> UI Canvas added to instigator's screen. Contains Widgets for text blocks and clickable button.
- UI Button "Next" clicked ==> UI Text Lines update to next in array.
- Last text lines in array shown ==> UI Button text updates to "Exit".
- UI Button "Exit" clicked ==> UI Canvas removed.
===== #>
ui_dialog_device := class(creative_device):
StringToMessage<localizes>(value:string) : message = "{value}"
#===== Devices =============================================================
@editable ButtonDialogStarter: button_device = button_device {} # Necessary.
@editable ItemGranter: item_granter_device = item_granter_device{} # Optional.
@editable AudioUIAdd: audio_player_device = audio_player_device{} # Optional.
@editable AudioUIRemove: audio_player_device = audio_player_device{} # Optional.
# ^ In Audio Player Devices settings, set "Can be heard by" to "Instigator Only". Uncheck "Enable Spatialization" and "Enable Volume Attenuation".
#===== Text for UI ========================================================
@editable TextButtonNext: string = ""
@editable TextButtonExit: string = ""
@editable TextTitle: string = ""
@editable TextLine1List: []string = array{}
@editable TextLine2List: []string = array{}
#===== Variables: Widgets for UI Canvas ======================================
var WidgetUIButton:button_regular=button_regular {}
var WidgetLine1:text_block = text_block {DefaultTextColor := NamedColors.DarkOrchid} #Change the color of the text here
var WidgetLine2:text_block = text_block {DefaultTextColor := NamedColors.DarkOrchid}
var WidgetTitle:text_block = text_block {DefaultTextColor := NamedColors.RoyalBlue}
var WidgetBackground: color_block = color_block {DefaultColor := NamedColors.White, DefaultOpacity := 0.70}
# ^ NamedColors keywords are from CSS Color Module Level 3. See: https://www.w3.org/TR/css-color-3/
#==== Variables: Other ======================================================
var MaybeMyUIPerPlayer : [player]?canvas = map{}
var currentTextLine : int = 0
# ==== Functions ==============================================================
# ==== Runs when the device is started in a running game =====================
OnBegin<override>()<suspends>:void=
Print("UI Dialogue Device began")
ButtonDialogStarter.InteractedWithEvent.Subscribe(HandleButtonUIAddInteraction)
WidgetUIButton.OnClick().Subscribe(HandleUIButtonClick)
WidgetTitle.SetText(StringToMessage(TextTitle))
WidgetUIButton.SetText(StringToMessage(TextButtonNext))
# ==== Runs when ButtonDialogStarter interacted with. ====================================
HandleButtonUIAddInteraction(Agent:agent):void=
Print("Button Device interacted with")
ButtonDialogStarter.Disable()
AddCanvas(Agent)
UpdateText(currentTextLine)
AudioUIAdd.Play(Agent) # Optional. Plays Audio Cue
# You could add other functions here too, like a Light, VFX, Cinematic Sequence, Switch, etc.
<# === When UI Button clicked, check if currentTextline int equals index int of the last member of the TextLine1List array.
(Aka "Is this the last part of dialog?"). If yes, call RemoveCanvas. If no, call UpdateText. #>
HandleUIButtonClick(Message: widget_message):void=
Print("UI Button clicked")
if (currentTextLine = TextLine1List.Length):
RemoveCanvas(Message)
set currentTextLine = 0
WidgetUIButton.SetText(StringToMessage(TextButtonNext))
else:
if (currentTextLine <= TextLine1List.Length - 1):
if (currentTextLine = TextLine1List.Length - 1):
WidgetUIButton.SetText(StringToMessage(TextButtonExit))
UpdateText(currentTextLine)
else:
UpdateText(currentTextLine)
<# === Update text in WidgetLine1 and WidgetLine2 text blocks, ====================
to the next members of the arrays.#>
UpdateText(Index:int):void=
Print("Update text")
if(currentStr := TextLine1List[Index]):
WidgetLine1.SetText(StringToMessage(currentStr))
if(currentStr := TextLine2List[Index]):
WidgetLine2.SetText(StringToMessage(currentStr))
set currentTextLine += 1
# ==== Add UI Canvas to the instigating player's screen.===========================
AddCanvas(Agent:agent):void=
Print("Add canvas")
if (InPlayer := player[Agent], PlayerUI := GetPlayerUI[InPlayer]):
if (MyUI := MaybeMyUIPerPlayer[InPlayer]?):
PlayerUI.RemoveWidget(MyUI)
if (set MaybeMyUIPerPlayer[InPlayer] = false) {}
else:
NewUI := CreateMyUI()
PlayerUI.AddWidget(NewUI, player_ui_slot{InputMode := ui_input_mode.All})
if (set MaybeMyUIPerPlayer[InPlayer] = option{NewUI}) {}
# ==== Sets the formatting for the UI Canvas, and returns the canvas. You can edit the Offsets, Alignment, and Padding. =================
# Canvas > Stack Box > Stack Box Slots > Widgets > Text.
CreateMyUI():canvas=
Print("Create UI")
NewCanvas := canvas:
Slots := array:
canvas_slot:
ZOrder := {1}
Anchors := anchors{ Maximum:= vector2{X:=1.0, Y:=1.0} }
Offsets := margin{ Top:=730.0, Left:= 630.0, Right:=630.0, Bottom := 0.0 } # Screen position of the UI canvas, measured in pixels from edges of a 1920 x 1080 screen.
Widget := stack_box:
Orientation := orientation.Vertical
Slots := array:
stack_box_slot:
HorizontalAlignment := horizontal_alignment.Left # Alignment of widget within slot.
Padding := margin{ Bottom:= 20.0} # Empty space between Title and Lines, in pixels.
Widget := WidgetTitle
stack_box_slot:
HorizontalAlignment := horizontal_alignment.Left
Padding := margin{ Left:= 50.0, Bottom := 10.0} # Optional left indent padding
Widget := WidgetLine1
stack_box_slot:
HorizontalAlignment := horizontal_alignment.Left
Padding := margin{ Left:= 50.0}
Widget := WidgetLine2
stack_box_slot:
HorizontalAlignment := horizontal_alignment.Right
Padding := margin{ Top:= 10.0} # Empty space between lines and button.
Widget := WidgetUIButton
canvas_slot:
ZOrder := {0}
Anchors := anchors{ Maximum:= vector2{X:=1.0, Y:=1.0} }
Offsets := margin{ Top:=700.0, Left:= 600.0, Right:=600.0, Bottom := 110.0 }
Widget := WidgetBackground
return NewCanvas
# ==== Remove UI Canvas from instigating player's screen. ============================================
RemoveCanvas(Message: widget_message):void=
if (PlayerUI := GetPlayerUI[Message.Player], MyUI := MaybeMyUIPerPlayer[Message.Player]?, SelectedButton := text_button_base[Message.Source]):
Print("Remove Canvas")
PlayerUI.RemoveWidget(MyUI)
if (set MaybeMyUIPerPlayer[Message.Player] = false) {}
ButtonDialogStarter.Enable()
AudioUIRemove.Play(Message.Player)
ItemGranter.GrantItem(Message.Player) # Optional. Grants gift/reward to Player.
<# ^ Call (Message.Player) instead of (Player), because our function RemoveCanvas is widget_message type not Player type.
Message.Player is a member of widget_message type. #>
#You could add other functions here too.
<# That's it :) Happy building <3 <3 <3
- Leave feedback/questions on my UI Dialog Device Forum post: https://forums.unrealengine.com/t/ui-dialog-verse-device-with-next-exit-button-snippet/744022
- Follow my Devlog for my UEFN game WIP, Sprigs: https://forums.unrealengine.com/t/sprigs-devlog-6-the-floor-is-lava-physical-material-experiments/657293
- Epic Docs webpage about Verse UI, for reference: https://dev.epicgames.com/documentation/en-us/uefn/creating-in-game-ui-in-verse #>
# from bug blaster the youtube video made by warforge
PropMoveToPlayer(Prop:creative_prop, Player: player)<suspends>:void=
MoveSpeed := 0.1
loop:
Sleep(0.03)
if (PlayerFort := Player.GetFortCharacter[], Prop.IsValid[])
then:
PropLocation := Prop.GetTransform().Translation
PlayerLocation := PlayerFort.GetTransform().Translation
if(LookDirection := (PlayerLocation - PropLocation).MakeUnitVector[]):
Yaw := RadiansToDegrees(ArcTan(LookDirection.Y, LookDirection.X))
Pitch := 0.0
Roll := 0.0
NewRotation := MakeRotationFromYawPitchRollDegrees(Yaw, Pitch, Roll)
LerpLocation:= Lerp(PropLocation, PlayerLocation, MoveSpeed)
FinalLocation := vector3 {X:= LerpLocation.X, Y:= LerpLocation.Y, Z:= 0.0}
else:
break
# from triad infiltrator
# FindTeamWithLargestDifference():?team =
# Print("Attempting to find smallest team")
# var TeamToAssign:?team = false
# var LargestDifference:int = 0
# for:
# CandidateTeam:Teams
# CurrentTeamSize := GetPlayspace().GetTeamCollection().GetAgents[CandidateTeam].Length
# MaximumTeamSize := TeamsAndTotals[CandidateTeam]
# do:
# Logger.Print("Checking a team...")
# Logger.Print("Maximum size Maximum size of team {CandidateTeamIndex + 1} is {MaximumTeamSize}")
# DifferenceFromMaximum := MaximumTeamSize - CurrentTeamSize
# Logger.Print("Difference from minimum is {DifferenceFromMaximum}")
# if(LargestDifference < DifferenceFromMaximum):
# set LargestDifference = DifferenceFromMaximum
# set TeamToAssign = option{CandidateTeam}
# Logger.Print("Found a team under minimum players: {DifferenceFromMaximum}")
# return TeamToAssign
# OnBegin<override>()<suspends> : void =
# # Get all the Teams
# set Teams = GetPlayspace().GetTeamCollection().GetTeams()
# set AllPlayers = GetPlayspace().GetPlayers()
# # Save the teams to later reference them
# set Infiltrators = option{Teams[0]}
# set Attackers = option{Teams[1]}
# set Defenders = option{Teams[2]}
# if:
# Infiltrators := InfiltratorsOpt?
# Attackers := AttackersOpt?
# Defenders := DefendersOpt?
# Logger.Print("Found all three teams")
# set TeamsAndTotals[Infiltrators] = MaximumInfiltrators
# set TeamsAndTotals[Attackers] = MaximumAttackers
# set TeamsAndTotals[Defenders] = MaximumDefenders
# Logger.Print("Set all three teams in TeamsAndTotals")
# then:
# set AllPlayers = Shuffle(AllPlayers)
# BalanceTeams()
# TeleportPlayersToStartLocations()
# else:
# Logger.Print("Couldn't find all teams, make sure to assign the correct teams in your island settings.")
triad_infiltration_log_channel := class(log_channel){}
asymmetric_multiplayer_balance := class(creative_device):
Logger:log = log{Channel := triad_infiltration_log_channel}
# To avoid players not being able to join a team, you should set the maximum number
@editable
MaximumInfiltrators:int = 2
@editable
MaximumAttackers:int = 3
@editable
MaximumDefenders:int = 3
var TeamsAndTotals:[team]int = map{}
@editable
Teleporters:[]teleporter_device = array{}
@editable
InvisibilityManager:invisibility_manager = invisibility_manager{}
@editable
var WeaponGranters:[]item_granter_device = array{}
@editable
PlayersSpawners:[]player_spawner_device = array{}
var InfiltratorsOpt:?team = false
var AttackersOpt:?team = false
var DefendersOpt:?team = false
var Teams:[]team = array{}
var AllPlayers:[]player = array{}
OnBegin<override>()<suspends> : void =
# Get all the Teams
set Teams = GetPlayspace().GetTeamCollection().GetTeams()
set AllPlayers = GetPlayspace().GetPlayers()
# Save the teams to later reference them
set InfiltratorsOpt = option{Teams[0]}
set AttackersOpt = option{Teams[1]}
set DefendersOpt = option{Teams[2]}
if:
Infiltrators := InfiltratorsOpt?
Attackers := AttackersOpt?
Defenders := DefendersOpt?
Logger.Print("Found all three teams")
set TeamsAndTotals[Infiltrators] = MaximumInfiltrators
set TeamsAndTotals[Attackers] = MaximumAttackers
set TeamsAndTotals[Defenders] = MaximumDefenders
Logger.Print("Set all three teams in TeamsAndTotals")
then:
set AllPlayers = Shuffle(AllPlayers)
#Subscribe to PlayerAddedEvent to allow team rebalancing when a new player joins the game
GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerAdded)
for(PlayerSpawner:PlayersSpawners):
PlayerSpawner.SpawnedEvent.Subscribe(OnPlayerSpawn)
BalanceTeams()
Logger.Print("Teams balanced, calling invisibility script")
InvisibilityManager.StartInvisibilityManager(Teams, AllPlayers, Infiltrators)
TeleportPlayersToStartLocations()
else:
Logger.Print("Couldn't find all teams, make sure to assign the correct teams in your island settings.")
# Grants players a weapon based on the index of their team in the Teams array
# by indexing into the WeaponGranters array.
GrantTeamWeapon(InPlayer:player):void=
if(CurrentTeam := GetPlayspace().GetTeamCollection().GetTeam[InPlayer]):
for(TeamIndex -> PlayerTeam:Teams, PlayerTeam = CurrentTeam):
if(WeaponGranter := WeaponGranters[TeamIndex]):
WeaponGranter.GrantItem(InPlayer)
Logger.Print("Granted the a Player on team {TeamIndex + 1} a weapon")
# Runs when any player spawns from a spawn pad.
# Calls GrantTeamWeapon using the provided SpawnedAgent.
OnPlayerSpawn(SpawnedAgent:agent):void=
if(SpawnedPlayer := player[SpawnedAgent]):
Logger.Print("Attempting to grant spawned player a weapon")
GrantTeamWeapon(SpawnedPlayer)
# Handles a new player joining the game
OnPlayerAdded(InPlayer:player):void=
Logger.Print("A new Player joined, assigning them to a team")
FortTeamCollection := GetPlayspace().GetTeamCollection()
set AllPlayers = GetPlayspace().GetPlayers()
# Assign the new player to the smallest team
BalancePlayer(InPlayer)
if (FortTeamCollection.GetTeam[InPlayer] = InfiltratorsOpt?):
InvisibilityManager.OnInfiltratorJoined(InPlayer)
<#
For each player, find the number of players of the team they're on. Iterate through the
list of teams and assign them to the team with the least amount of players, or their
starting team in case of ties.
#>
BalanceTeams():void=
Logger.Print("Beginning to balance teams")
FortTeamCollection := GetPlayspace().GetTeamCollection()
Logger.Print("AllPlayers Length is {AllPlayers.Length}")
for (TeamPlayer : AllPlayers):
BalancePlayer(TeamPlayer)
BalancePlayer(InPlayer:player):void=
var TeamToAssign:?team = false
set TeamToAssign = FindTeamWithLargestDifference()
if (AssignedTeam := TeamToAssign?, GetPlayspace().GetTeamCollection().AddToTeam[InPlayer, AssignedTeam]):
Logger.Print("Attempting to assign newly joined to a new team")
else:
Logger.Print("This player was already on the smallest team")
# Finds the team with the largest difference in their number of players from their
# maximum number of players
FindTeamWithLargestDifference():?team =
Logger.Print("Attempting to find smallest team")
var TeamToAssign:?team = false
var LargestDifference:int = 0
for:
CandidateTeamIndex -> CandidateTeam:Teams
CurrentTeamSize := GetPlayspace().GetTeamCollection().GetAgents[CandidateTeam].Length
MaximumTeamSize := TeamsAndTotals[CandidateTeam]
do:
Logger.Print("Checking a team...")
Logger.Print("Maximum size of team {CandidateTeamIndex + 1} is {MaximumTeamSize}")
DifferenceFromMaximum := MaximumTeamSize - CurrentTeamSize
Logger.Print("Difference from maximum is {DifferenceFromMaximum}")
if(LargestDifference < DifferenceFromMaximum):
set LargestDifference = DifferenceFromMaximum
set TeamToAssign = option{CandidateTeam}
Logger.Print("Found team {CandidateTeamIndex + 1} with difference {DifferenceFromMaximum}")
return TeamToAssign
TeleportPlayersToStartLocations():void=
Logger.Print("Teleporting players to start locations")
for:
TeamIndex -> PlayerTeam:Teams
TeamPlayers := GetPlayspace().GetTeamCollection().GetAgents[PlayerTeam]
TeamTeleporter := Teleporters[TeamIndex]
Transform := TeamTeleporter.GetTransform()
do:
for(TeamPlayer:TeamPlayers):
TeamPlayer.Respawn(Transform.Translation, Transform.Rotation)
Logger.Print("Teleported this player to their start location")
triad_invisibility_log_channel := class(log_channel){}
invisibility_manager := class(creative_device):
Logger:log = log{Channel := triad_invisibility_log_channel}
@editable
PlayersSpawners:[]player_spawner_device = array{}
# Whether the visibility of the infiltrators is shared with teammates.
@editable
IsVisibilityShared:logic = true
# How long the infiltrators are visible for after being damaged.
@editable
VulnerableSeconds:float = 3.0
# How quickly infiltrators flicker after being damaged
@editable
FlickerRateSeconds:float = 0.8
var Teams:[]team = array{}
var PlayerVisibilitySeconds:[agent]float = map{}
OnBegin<override>()<suspends>:void=
# Wait for teams to be balanced before subscribing to events that make the players invisible.
Logger.Print("Waiting for teams to be balanced...")
StartInvisibilityManager<public>(AllTeams:[]team, Players:[]player, Infiltrators:team):void=
Logger.Print("Invisibility script started!")
set Teams = AllTeams
AllPlayers := Players
for(PlayerSpawner:PlayersSpawners):
PlayerSpawner.SpawnedEvent.Subscribe(OnPlayerSpawn)
<# For each player, if they spawned on the infiltrator team, spawn an OnInfiltratorDamaged function for that
player. Then make their character invisible. #>
for(TeamPlayer : AllPlayers):
if:
FortCharacter:fort_character = TeamPlayer.GetFortCharacter[]
CurrentTeam := GetPlayspace().GetTeamCollection().GetTeam[TeamPlayer]
Logger.Print("Got this player's current team")
Infiltrators = CurrentTeam
set PlayerVisibilitySeconds[TeamPlayer] = 0.0
Logger.Print("Added player to PlayerVisibilitySeconds")
then:
spawn{OnInfiltratorDamaged(TeamPlayer)}
Logger.Print("Player spawned as an infiltrator, making them invisible")
FortCharacter.Hide()
else:
Logger.Print("This player isn't an infiltrator")
FlickerCharacter(InCharacter:fort_character)<suspends>:void=
Logger.Print("FlickerCharacter() invoked")
# Loop hiding and showing the character to create a flickering effect.
loop:
InCharacter.Hide()
Sleep(FlickerRateSeconds)
InCharacter.Show()
Sleep(FlickerRateSeconds)
# Each loop, decrease the amount of time the character is flickering by FlickerRateSeconds.
# If Remaining time hits 0, break out of the loop.
if:
TimeRemaining := set PlayerVisibilitySeconds[InCharacter.GetAgent[]] -= FlickerRateSeconds * 2
TimeRemaining <= 0.0
then:
InCharacter.Hide()
break
OnInfiltratorDamaged(InAgent:agent)<suspends>:void=
Logger.Print("Attempting to start flickering this character")
TeamCollection := GetPlayspace().GetTeamCollection()
if (FortCharacter := InAgent.GetFortCharacter[]):
loop:
if (IsVisibilityShared?, CurrentTeam := TeamCollection.GetTeam[InAgent], TeamAgents := TeamCollection.GetAgents[CurrentTeam]):
# For each teammate, set them in PlayerVisibility seconds and spawn a FlickerEvent
for(Teammate : TeamAgents):
Logger.Print("Calling StartOrResetFlickering on a Teammate")
StartOrResetFlickering(Teammate)
else:
# Just flicker the damaged character
Logger.Print("Calling StartOrResetFlickering on InAgent")
StartOrResetFlickering(InAgent)
FortCharacter.DamagedEvent().Await()
StartOrResetFlickering(InAgent:agent):void=
if (not IsFlickering[InAgent], FortCharacter := InAgent.GetFortCharacter[]):
Logger.Print("Attempting to start NEW FlickerEvent for this character")
# New flickering started
if (set PlayerVisibilitySeconds[InAgent] = VulnerableSeconds):
spawn{FlickerCharacter(FortCharacter)}
Logger.Print("Spawned a FlickerEvent for this character")
else:
# Reset ongoing flickering
if (set PlayerVisibilitySeconds[InAgent] = VulnerableSeconds):
Logger.Print("Reset character's FlickerTimer to VulnerableSeconds")
IsFlickering(InAgent:agent)<decides><transacts>:void=
PlayerVisibilitySeconds[InAgent] > 0.0