You can’t do a real “tick”, and for good reason IMO (see my opinion on why below the code and log output), but you can do this…
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
# Simple example of a device that acts as a "service" for your game mechanic(s)
# Verse Protip: If operating within a <suspends> function, be sure to call setters with <transacts> effect when you want anything to persist/commit!
service_device := class(creative_device) {
var Active : logic = true # start ticking immediately
var UpdateNowSecs<private> : float = 0.0
var UpdatePrevSecs<private> : float = 0.0
var UpdateDeltaSecs<private> : float = 0.0
GetUpdateDeltaSecs()<transacts> : float = { return UpdateDeltaSecs }
var WorkQueueCount : int = 0 # just some fake value representing "work to be done"
var FrameNumber : int = 0
GetFrameNumber()<transacts> : int = { return FrameNumber }
OnBegin<override>()<suspends> : void = {
Print("OnBegin, frame {FrameNumber}")
# Skipping first frame is usually required if you want to interact with other devices immediately (they might not have been loaded by Fortnite yet)
Sleep(0.0)
spawn:
WorkerThread()
set UpdatePrevSecs = GetSimulationElapsedTime()
loop {
# note that we don't call "Setters" with <transacts> on these, we have <transacts> on the getter instead since you might not even use it
set UpdateNowSecs = GetSimulationElapsedTime()
set UpdateDeltaSecs = (UpdateNowSecs - UpdatePrevSecs)
set UpdatePrevSecs = UpdateNowSecs
OnUpdate()
set FrameNumber = FrameNumber + 1
Sleep(0.0)
}
}
OnEnd<override>()<transacts> : void = {
set Active = false
}
# Loops forever, many times per tick (depends how long DoWork goes for). Essentially a second thread. Will also fire BEFORE the first game tick, so be careful.
WorkerThread()<suspends> : void = {
loop {
if (Active = false) {
break
} else {
DoAllPendingWork()
Sleep(0.0)
}
}
}
# Called every game tick (~30 per second), but after render.
OnUpdate()<suspends> : void = {
Print("OnUpdate; Frame = {GetFrameNumber()}; Delta = {GetUpdateDeltaSecs()}")
set WorkQueueCount = 10 # set the fake work value to 10
}
HasWork()<transacts> : logic = {
# just a sample, there is no work this tick
return (if (WorkQueueCount > 0) { true } else { false } )
#if (WorkQueueCount > 0) { return true } else { return false }
}
DoWork()<transacts> : void = {
set WorkQueueCount = WorkQueueCount - 1
}
DoAllPendingWork()<suspends> : void = {
loop {
if (HasWork() = true) {
# Maybe poll a queue, iterate an array, etc...
# ! Be aware of contention, otherwise you'll nerf throughput. Try not to operate on game objects or too much data; e.g. use logic
# flags like dirty/changed and cache values from OnUpdate method where possible.
Print("DoWork {WorkQueueCount} ...")
DoWork()
} else { return }
}
}
}
…if you place this device and run as-is, your Print/Log output looks like this:
LogVerse: : OnBegin, frame 0
LogVerse: : OnUpdate; Frame = 0; Delta = 0.000000
LogVerse: : DoWork 10 ...
LogVerse: : DoWork 9 ...
LogVerse: : DoWork 8 ...
LogVerse: : DoWork 7 ...
LogVerse: : DoWork 6 ...
LogVerse: : DoWork 5 ...
LogVerse: : DoWork 4 ...
LogVerse: : DoWork 3 ...
LogVerse: : DoWork 2 ...
LogVerse: : DoWork 1 ...
LogVerse: : OnUpdate; Frame = 1; Delta = 0.033394
LogVerse: : DoWork 10 ...
LogVerse: : DoWork 9 ...
LogVerse: : DoWork 8 ...
LogVerse: : DoWork 7 ...
LogVerse: : DoWork 6 ...
LogVerse: : DoWork 5 ...
LogVerse: : DoWork 4 ...
LogVerse: : DoWork 3 ...
LogVerse: : DoWork 2 ...
LogVerse: : DoWork 1 ...
LogVerse: : OnUpdate; Frame = 2; Delta = 0.033399
LogVerse: : DoWork 10 ...
LogVerse: : DoWork 9 ...
LogVerse: : DoWork 8 ...
LogVerse: : DoWork 7 ...
LogVerse: : DoWork 6 ...
LogVerse: : DoWork 5 ...
LogVerse: : DoWork 4 ...
LogVerse: : DoWork 3 ...
LogVerse: : DoWork 2 ...
LogVerse: : DoWork 1 ...
LogVerse: : OnUpdate; Frame = 3; Delta = 0.033414
LogVerse: : DoWork 10 ...
LogVerse: : DoWork 9 ...
LogVerse: : DoWork 8 ...
LogVerse: : DoWork 7 ...
LogVerse: : DoWork 6 ...
LogVerse: : DoWork 5 ...
LogVerse: : DoWork 4 ...
LogVerse: : DoWork 3 ...
LogVerse: : DoWork 2 ...
LogVerse: : DoWork 1 ...
LogVerse: : OnUpdate; Frame = 4; Delta = 0.033400
LogVerse: : DoWork 10 ...
LogVerse: : DoWork 9 ...
LogVerse: : DoWork 8 ...
LogVerse: : DoWork 7 ...
LogVerse: : DoWork 6 ...
LogVerse: : DoWork 5 ...
LogVerse: : DoWork 4 ...
LogVerse: : DoWork 3 ...
LogVerse: : DoWork 2 ...
LogVerse: : DoWork 1 ...
LogVerse: : OnUpdate; Frame = 5; Delta = 0.033398
LogVerse: : OnUpdate; Frame = 6; Delta = 0.033452
LogVerse: : DoWork 10 ...
LogVerse: : DoWork 9 ...
LogVerse: : DoWork 8 ...
LogVerse: : DoWork 7 ...
LogVerse: : DoWork 6 ...
…do be aware that the delta’s are kinda high (should be at or below 0.33333, being a 30 tick game) because the Print operation is kinda expensive. Also that skipped update between frame 5 and 6 could be an issue depending on what you want to do, though that can be solved… I’m only giving a basic example.
Anyway, you can do a LOT with this.
Having our own events for OnPlayerMovement and OnPlayerAttack and such would be good. Avoids having to use round-about ways (e.g. measuring distances between things, stuck-to-player creative devices…)
A tick function is not necessary. Not only that, but it shifts responsibility of the core game logic to our own verse code - which is not good for performance and “cloud gaming platform” reasons. We should be able to achieve everything with Events, and I think it’s fine that we can’t roll roll our own solution for Events that are lacking. We have so many other platforms that let you do a real Tick method, Core for example. And it’s so much more complicated because of it.; needing to facilitate that.
…that’s just my opinion though. Maybe as a compromise, they could allow us to have one Tick event per game, and just limit how many operations we can do per-tick (to encourage using async stuff). But I have a feeling it’s much easier said than done, given the hosted/cloud-based/GaaS nature of Fortnite.