Skip to content

Commit

Permalink
Separate scheduling logic for the beginning and end of a frame
Browse files Browse the repository at this point in the history
  • Loading branch information
YetAnotherClown committed Aug 26, 2024
1 parent fdfe525 commit 32bda12
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 54 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 33 additions & 12 deletions docs/getting-started/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,59 @@ import TabItem from '@theme/TabItem';

# Hooks

<Tabs>
<TabItem value="lua" label="Luau" default>
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.

<Tabs>
<TabItem value="lua" label="Luau" default>

```lua
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)
```

</TabItem>
<TabItem value="ts" label="Typescript">

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();
});
```

</TabItem>
Expand Down
30 changes: 26 additions & 4 deletions docs/setup/other.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```

</TabItem>
Expand All @@ -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();
});
```

</TabItem>
Expand Down
48 changes: 42 additions & 6 deletions lib/Bridge.luau
Original file line number Diff line number Diff line change
Expand Up @@ -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 }?,
Expand Down Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions lib/__tests__/Bridge.test.luau
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
53 changes: 35 additions & 18 deletions lib/__tests__/Route.test.luau
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -54,7 +66,7 @@ describe("Route", function()
Channel = "Reliable",
}) :: Net.Route<number, boolean, string>

serverHook = createHook({ serverRoute }, "server")
serverBeginFrame, serverEndFrame = createHook({ serverRoute }, "server")

-- Setup client

Expand All @@ -65,26 +77,27 @@ describe("Route", function()
}) :: Net.Route<number, boolean, string>
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()
Expand All @@ -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()

Expand All @@ -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()

Expand All @@ -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()

Expand Down
Loading

0 comments on commit 32bda12

Please sign in to comment.