diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fff609..f68d6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- `Bridge:step()` has been replaced by `Bridge:beginFrame` and `Bridge:endFrame` which separates the logic behind the Incoming and Outgoing queues +- `YetAnotherNet.createHook()` now returns two functions, a `beginFrame` and a `endFrame` function which separates scheduling logic + ## [0.9.0] - 2024-07-11 ### Added diff --git a/docs/getting-started/hooks.mdx b/docs/getting-started/hooks.mdx index b37ba12..de79e5f 100644 --- a/docs/getting-started/hooks.mdx +++ b/docs/getting-started/hooks.mdx @@ -7,12 +7,15 @@ import TabItem from '@theme/TabItem'; # Hooks - - +Hooks allow you full control of scheduling logic, allowing YetAnotherNet to be used for any game structure whether ECS or not. -Hooks allow you to integrate YetAnotherNet into any game architecture you want. These are simply functions that you can call whenever you want to process your Packets. It's recommended that you set your hooks to run on the Heartbeat using `RunService.Heartbeat`, so your Networking Code can be scheduled to run frame-by-frame as Net was designed to do. +When you use `createHook`, it will return a `beginFrame` and `endFrame` function which should be called at the beginning and end of each frame respectively. -To create a hook, you can use ``YetAnotherNet.createHook({ Route })`` and pass in a table of your Routes, then you can call it whenever you want to process your packets. +It's expected, and recommended that you still run your scheduling code on `RunService.Heartbeat`, otherwise you may run into unexpected behavior. If you know +what you're doing, you can ignore this warning. + + + ```lua local RunService = game:GetService("RunService") @@ -20,25 +23,43 @@ local RunService = game:GetService("RunService") local YetAnotherNet = require("@packages/YetAnotherNet") local routes = require("@shared/routes") -local hook = YetAnotherNet.createHook(routes) -RunService.Heartbeat:Connect(hook) +local myRoute = routes.myRoute + +local beginFrame, endFrame = YetAnotherNet.createHook({ Route }) +RunService.Heartbeat:Connect(function() + beginFrame() + + myRoute:send(...) + for i, player, data in myRoute:query() do + -- Do something + end + + endFrame() +end) ``` -Hooks allow you to integrate YetAnotherNet into any game architecture you want. These are simply functions that you can call whenever you want to process your Packets. It's recommended that you set your hooks to run on the Heartbeat using `RunService.Heartbeat`, so your Networking Code can be scheduled to run frame-by-frame as Net was designed to do. - -To create a hook, you can use ``YetAnotherNet.createHook({ route: Route })`` and pass in an array of your Routes, then you can call it whenever you want to process your packets. - ```ts import { RunService } from "@rbxts/services"; import Net from "@rbxts/yetanothernet"; import routes from "shared/routes"; -const hook = Net.createHook(routes); -RunService.Heartbeat.Connect(hook); +const myRoute = routes.myRoute; + +const beginFrame, endFrame = Net.createHook(routes); +RunService.Heartbeat.Connect(() => { + beginFrame(); + + myRoute.send(...) + for (const [pos, sender, ...] of myRoute.query()) { + // Do something + } + + endFrame(); +}); ``` diff --git a/docs/setup/other.mdx b/docs/setup/other.mdx index d3dac49..89a7431 100644 --- a/docs/setup/other.mdx +++ b/docs/setup/other.mdx @@ -19,8 +19,19 @@ local RunService = game:GetService("RunService") local YetAnotherNet = require("@packages/YetAnotherNet") local routes = require("@shared/routes") -local hook = YetAnotherNet.createHook(routes) -RunService.Heartbeat:Connect(hook) +local myRoute = routes.myRoute + +local beginFrame, endFrame = YetAnotherNet.createHook({ Route }) +RunService.Heartbeat:Connect(function() + beginFrame() + + myRoute:send(...) + for i, player, data in myRoute:query() do + -- Do something + end + + endFrame() +end) ``` @@ -35,8 +46,19 @@ import { RunService } from "@rbxts/services"; import Net from "@rbxts/yetanothernet"; import routes from "shared/routes"; -const hook = Net.createHook(routes); -RunService.Heartbeat.Connect(hook); +const myRoute = routes.myRoute; + +const beginFrame, endFrame = Net.createHook(routes); +RunService.Heartbeat.Connect(() => { + beginFrame(); + + myRoute.send(...) + for (const [pos, sender, ...] of myRoute.query()) { + // Do something + } + + endFrame(); +}); ``` diff --git a/lib/Bridge.luau b/lib/Bridge.luau index e713bdb..8f8262a 100644 --- a/lib/Bridge.luau +++ b/lib/Bridge.luau @@ -64,7 +64,8 @@ export type BridgeImpl = { _outgoingQueue: OutgoingQueue, _snapshot: IncomingQueue, - step: (self: Bridge) -> (), + beginFrame: (self: Bridge) -> (), + endFrame: (self: Bridge) -> (), snapshot: (self: Bridge) -> IncomingQueue, _getInstanceMapChanges: (self: Bridge) -> { [number]: Instance }?, @@ -291,23 +292,58 @@ function Bridge:snapshot() end --[=[ - @method step + @method beginFrame @within Bridge - - This will empty the IncomingQueue and produce a new Snapshot of it, then - it will process the OutgoingQueue and send the payloads over the network. + The IncomingQueue is a queue that collects all the incoming data from a frame, + the use of this function creates a new snapshot of it and then empties the queue. + This snapshot is what your Routes will use to read the data that was sent in the last frame. + + :::note + Assuming all scheduling code and the use of `send` and `query` are running on the Heartbeat, + this will not actually cause any delay for when you recieve your data, as Replication Events are sent + after the Heartbeat. + See [Schedular Priority](https://create.roblox.com/docs/studio/microprofiler/task-scheduler#scheduler-priority) + for more information. + ::: + + :::warning You should only use this function if creating custom scheduling behavior similar to the Hooks API, which you should use instead of trying to achieve this behavior using the Bridge itself. + ::: ]=] -function Bridge:step() +function Bridge:beginFrame() self._snapshot = table.freeze(self._incomingQueue) self._incomingQueue = { Reliable = {}, Unreliable = {}, } +end +--[=[ + @method endFrame + @within Bridge + + The OutgoingQueue collects all the outgoing data from a frame, when you do + `Route:send()` the data is not immediately sent over the network, it is instead + batched and sent over the network at the end of the frame. + + :::note + Assuming all scheduling code and the use of `send` and `query` are running on the Heartbeat, + this will not actually cause any delay for when you recieve your data, as Replication Events are sent + after the Heartbeat. + See [Schedular Priority](https://create.roblox.com/docs/studio/microprofiler/task-scheduler#scheduler-priority) + for more information. + ::: + + :::warning + You should only use this function if creating custom scheduling behavior + similar to the Hooks API, which you should use instead of trying to achieve + this behavior using the Bridge itself. + ::: +]=] +function Bridge:endFrame() local clientPayloads, serverPayloads = self:_processOutgoingQueue() self:_sendPayloads(clientPayloads, serverPayloads) end diff --git a/lib/__tests__/Bridge.test.luau b/lib/__tests__/Bridge.test.luau index dbabe72..25c1346 100644 --- a/lib/__tests__/Bridge.test.luau +++ b/lib/__tests__/Bridge.test.luau @@ -56,8 +56,10 @@ describe("Bridge", function() queueFakePackets(serverBridge, player) - serverBridge:step() - clientBridge:step() + serverBridge:endFrame() + clientBridge:endFrame() + serverBridge:beginFrame() + clientBridge:beginFrame() local snapshot = clientBridge:snapshot() @@ -163,7 +165,8 @@ describe("Bridge", function() test("Should not send empty payloads", function() -- Clear queue - clientBridge:step() + clientBridge:endFrame() + clientBridge:beginFrame() -- Track whether or not the RemoteEvent was fired local wasFired = false @@ -194,8 +197,10 @@ describe("Bridge", function() data = { instance }, }) - serverBridge:step() - clientBridge:step() + serverBridge:endFrame() + clientBridge:endFrame() + serverBridge:beginFrame() + clientBridge:beginFrame() expect(clientBridge._instanceMap).toContain(instance) diff --git a/lib/__tests__/Route.test.luau b/lib/__tests__/Route.test.luau index 39c8fab..a5acd9e 100644 --- a/lib/__tests__/Route.test.luau +++ b/lib/__tests__/Route.test.luau @@ -17,13 +17,16 @@ local mockBridge = require("@yetanothernet/__mocks__/mockBridge") local MockedBridge local serverRoute -local serverHook +local serverBeginFrame +local serverEndFrame local clientRoute -local clientHook +local clientBeginFrame +local clientEndFrame local mockPlayer -local processHooks +local beginFrame +local endFrame describe("Route", function() beforeAll(function() @@ -38,14 +41,23 @@ describe("Route", function() ) local bridge = MockedBridge.new(context, player) - return function() + local function beginFrame() + bridge:beginFrame() + for _, route in routes do route:_updateSnapshot(bridge) + end + end + + local function endFrame() + for _, route in routes do route:_queuePackets(bridge) end - bridge:step() + bridge:endFrame() end + + return beginFrame, endFrame end -- Setup server @@ -54,7 +66,7 @@ describe("Route", function() Channel = "Reliable", }) :: Net.Route - serverHook = createHook({ serverRoute }, "server") + serverBeginFrame, serverEndFrame = createHook({ serverRoute }, "server") -- Setup client @@ -65,26 +77,27 @@ describe("Route", function() }) :: Net.Route clientRoute["_identifier"] = serverRoute["_identifier"] - clientHook = createHook({ clientRoute }, "client", mockPlayer) + clientBeginFrame, clientEndFrame = createHook({ clientRoute }, "client", mockPlayer) - processHooks = function() - serverHook() - clientHook() + beginFrame = function() + serverBeginFrame() + clientBeginFrame() + end - -- To account for data being deferred by one frame - serverHook() - clientHook() + endFrame = function() + serverEndFrame() + clientEndFrame() end end) -- Schedule cleanup beforeEach(function() - processHooks() + beginFrame() end) -- Schedule cleanup afterEach(function() - processHooks() + endFrame() end) test("When calling `:send()`, it should properly queue the packets to it's bridge", function() @@ -101,7 +114,9 @@ describe("Route", function() test("When calling `:query()`, it should return a valid QueryResult", function() local sendRequest = serverRoute:send(1, true, "Hello, world") sendRequest:to(mockPlayer) - processHooks() + + endFrame() + beginFrame() local queryResult = clientRoute:query() @@ -128,7 +143,8 @@ describe("Route", function() local sendRequest = serverRoute:send(false :: any, "Oops" :: any, 1 :: any) sendRequest:to(mockPlayer) - processHooks() + endFrame() + beginFrame() local queryResult = clientRoute:query() @@ -155,7 +171,8 @@ describe("Route", function() local sendRequest = serverRoute:send(false :: any, "Oops" :: any, 1 :: any) sendRequest:to(mockPlayer) - processHooks() + endFrame() + beginFrame() local queryResult = clientRoute:query() diff --git a/lib/init.luau b/lib/init.luau index 910ba79..d314075 100644 --- a/lib/init.luau +++ b/lib/init.luau @@ -429,31 +429,51 @@ end --[=[ @within YetAnotherNet - This function allows you to run Net scheduling code on your own events. + This function allows you to run the scheduling code on your own events. - When you provide a table of Routes, this function will return another function - you can call which will step each Route and process it's Packet Queue. + Because scheduling should be ran at the beginning and end of each frame, + this will return two functions which you can use to call the scheduling + code for the beginning and end of a frame. For example, to run scheduling on the Heartbeat: ```lua - local hook = YetAnotherNet.createHook({ Route }) - RunService.Heartbeat:Connect(hook) + local beginFrame, endFrame = YetAnotherNet.createHook({ Route }) + RunService.Heartbeat:Connect(function() + beginFrame() + + Route:send(...) + for i, player, data in Route:query() do + -- Do something + end + + endFrame() + end) ``` - @param routes { Route } -- A table of your Routes - @return () -> () + @param routes { Route } -- A table of Routes to run with these hooks + @return () -> () -- Begin frame hook + @return () -> () -- End frame hook ]=] function createHook(routes) local bridge = Bridge.new() - return function() + local function beginFrame() + bridge:beginFrame() + for _, route in routes do route:_updateSnapshot(bridge) + end + end + + local function endFrame() + for _, route in routes do route:_queuePackets(bridge) end - bridge:step() + bridge:endFrame() end + + return beginFrame, endFrame end --- @prop server "Net_Server"