Skip to content

Commit

Permalink
Docs
Browse files Browse the repository at this point in the history
# Docs
Overhauled and redesigned documentation with better pages and documentation.

## Additions
- Lifecycle documentation page
- State documentation page
- Common issues documentation page
- Getting started documentation page
- Showcase page

## Changes
- Overhauled intro page with better examples
- Rewrote widgets documentation pages
- Updated events documentation pages
- Updated copyright

## Fixes
- Standardised error messages
- Many typos and errors in existing API documentation
  • Loading branch information
SirMallard authored Dec 9, 2024
2 parents e0a731b + 0f4805a commit d28421b
Show file tree
Hide file tree
Showing 24 changed files with 1,039 additions and 220 deletions.
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
* text=auto
* text=auto eol=crlf
*.lua text eol=crlf linguist-language=Luau
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
bin/
build/
node_modules/
.vscode/

*.rbxl
*.rbxlx
*.rbxm
*.rbxmx
*.lock
package*

sourcemap.json
Binary file removed assets/IrisHelpfulChart.png
Binary file not shown.
Binary file added assets/simple-example1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/simple-example2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/about/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "About",
"position": 6
}
155 changes: 155 additions & 0 deletions docs/about/cycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
sidebar_position: 1
---

# Understanding the Lifecycle

## General Game Lifecycle

Iris is designed for games with a core 'game loop' which is the structure that controls what
part of the game process happens when. A typical game loop make look very similar to this:

```cpp
while(not_closed) {
poll_input();
update_game_state();
step_physics();
render_content();
wait(); // for a 60 fps limit
}
```
Here we start firstly with polling for any input changes, since these affect the game state
for that frame. We then update the game state which generally includes the majority of a
game engine, since it would control any user updates, world changes, UI updates and others.
We may also then choose to step our physics engine, assuming we are using a constant frame
rate. Finally we render out everything to our GPU and wait until the appropriate time to
start processing the next frame.

Roblox takes most of this away from developers, and instead chooses to rely on an event-driven
loop, where we hook onto a part of the engine allowing something else to happen. This makes it
more difficult to use Iris, since not every place we want it will run every frame. However,
Roblox provides access to RunService events, allowing us to execute code every frame, which is
seen below:

```lua
while not_closed do
update(UserInputService)
update(ContextActionService)

event(RunService.BindToRenderStepped)
event(RunService.RenderStepped)

render()

event(wait)
event(RunService.Stepped)
update(PhysicsService)

event(RunService.Heartbeat)
update(ReplicationService)

delay() -- for a 60 fps limit
end
```

This is taken from the [Task Scheduler Documentation](https://create.roblox.com/docs/studio/microprofiler/task-scheduler)
which goes into more detail about this.

## Iris Lifecycle

Iris needs to run every frame, called the cycle, in order to update global variables and to
clean any unused widgets. This is equivalent to calling `ImGui::EndFrame()` for Dear ImGui,
which would then process the frame buffers ready for use. This order is important for Iris,
which by default uses the `RunService.Heartbeat` event to process this all on. Therefore, for
each frame, any Iris code must run before this event. It is possible to change the event Iris
runs on when initialising, but for most cases, `RunService.Heartbeat` is ideal.

Understanding this is the key to most effectively using Iris. The library provides a handy
`Iris:Connect()` function which will run any code in the function every frame before the
cycle. This makes it the most convenient. However, any functions provided here will also run
on the initialised event, `RunService.Heartbeat` here, so will run after physics and animations
are calculated. Thankfully, Iris does not constrain you to use only `Iris:Connect()`. You are
able to run Iris code anywhere, in any event, at any time. As long as it is consistent on
every frame, and before the cycle event, it will work properly. Therefore, it is very possible
to put Iris directly into your core game loops.

## Demonstration

Say you have a weapon class which is used by every weapon and then also a weapon handler/serivce/system/controller
for handling all weapons on the client. Integrating Iris may look something similar to this:
```lua
------------------------------------------------------------------------
--- game.ReplicatedStorage.Modules.Client.Weaopns.WeaponsService.lua
------------------------------------------------------------------------
local WeaponsService = {
maxWeapons = 10,
activeWeapon = nil,
weapons = {}
}

function WeaponsService.init()
end

-- called every frame to update all weapons
function WeaponsService.update(deltaTime: number)
Iris.Window({ "Weapons Service" })

WeaponsService.doSomething()
Iris.CollapsingHeader({ "Global Variables" })
Iris.DragNum({ "Max Weapons", 1, 0 }, { number = Iris.TableState(WeaponsService.maxWeapons) })
Iris.End()

Iris.CollapsingHeader({ "Weapons" })
Iris.Tree({ `Active Weapon: {WeaponsService.activeWeapon.name}` })
WeaponsService.activeWeapon:update()
Iris.End()

Iris.SeparatorText({ "All Weapons" })
for _, weapon: weapon in WeaponsService.weapons do
Iris.Tree({ weapon.name })
weapon:update()
Iris.End()
end
Iris.End()

WeaponsService.doSomethingElse()
Iris.End()
end

function WeaponsService.terminate()
end

return WeaponsService

------------------------------------------------------------------------
--- game.ReplicatedStorage.Modules.Client.Weaopns.Weapon.lua
------------------------------------------------------------------------
local Weapon = {}
Weapon.__index = Weapon

function Weapon.new(...)
end

function Weapon.update(self, deltaTime: number)
Iris.Text({ `ID: {self.id}` })
Iris.Text({ `Bullets: {self.bullets}/{self.capacity}" })
Iris.Checkbox({ "No reload" }, { isChecked = Iris.TableState(self.noreload) })
...
self:updateInputs()
self:updateTransforms()
...
end

function Weapon.destroy(self)
end

```

Although this is very bare bones code, we are not using any `Iris:Connect()` methods
and instead place our Iris code directly in our update events which we know will run
every frame. Another practice this shows is starting a window somewhere and keeping it
open through all weapons before closing it and the end of the update. Therefore, we
can place lots of different widgets in one window and keep everything organised.

The showcase by [@Boogle](https://x.com/LeBoogle/status/1772384187426709879) shows
off Iris used exactly like this, but with an actual working system.
15 changes: 10 additions & 5 deletions docs/events.md → docs/about/events.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Events
---
sidebar_position: 4
---

# Understanding Events

Each widget has a number of events connected to it. You can see these events on the [API page](/API/Iris).

Certain events will happen once, such as a window being collapsed or a button being clicked. Other events can be continuous, such as a widget being hovered.
Each event is a function which returns a boolean value for whether the event has happened that frame or not.
Certain events will happen once, such as a window being collapsed or a button being clicked. Other events can be
continuous, such as a widget being hovered. Each event is a function which returns a boolean value for whether the
event has happened that frame or not.

To listen to an event, use the following:
```lua
Expand All @@ -13,8 +18,8 @@ if button.clicked() then
end
```

Events will fire the frame after the initial action happened. This is so that any changes caused by that event can propogate visually.
For example on a checkbox:
Events will fire the frame after the initial action happened. This is so that any changes caused by that event can
propogate visually. For example on a checkbox:

- [Frames 1 - 60]
The mouse is elsewhere.
Expand Down
71 changes: 71 additions & 0 deletions docs/about/states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
sidebar_position: 3
---

# Understanding State

An Iris State object is simply a table containg a value, and an array of connected widgets. It provides functions to
get or set the value, the latter of which will update any widgets UI that are dependent on that state. Functions can
also be connected which will be fired whenever the value changes.

A state object ultimately attempts to copy the behaviour that a pointer would do in other languages, but is not
possible in native Luau. A Luau table is the best option, because it is passed by reference.

## Types of State

Iris provides multiple different types of State objects, suited for different needs.

### State

The base and most common state type, which implements the basically functionality of any state object.

### WeakState

A WeakState is very similar to State, except that every time it is called by ID, using `Iris.WeakState()`, all
connected widgets and functions are removed, whilst keeping the value. This is useful if you need to disconnect any
widgets from a state, so that they no longer update, whilst also keeping the existing value.

### VariableState

A VariableState takes both a value, and a function which gives the new value of the state whenever it is changed. This
is designed for when you have a variable within a file, and want to link it to a state object. By default, when the
function is called, if the variable and state are different, it will choose the local variable value. But if the state
is changed, it will use the callback which is designed to update the local variable.

This is best shown with an example:
```lua
local myNumber = 5

local state = Iris.VariableState(myNumber, function(value)
myNumber = value
end)
Iris.DragNum({ "My number" }, { number = state })
```

Here we create a state for a DragNum. If we update the value of `myNumber` within the code earlier, it will update the
state value. And if we drag the widget, and update the state, it will call our callback, where we update the value of
`myNumber`.

### TableState

A TableState acts like VariableState, but takes a table and index so that whenever the table value changes, the state
changes and vice versa. Because tables are shared, we do not need to provide a function to update the table value, and
is instead handled internally.

We can see this with an example:
```lua
local data = {
myNumber = 5
}

local state = Iris.TableState(data, "myNumber")
Iris.DragNum({ "My number" }, { number = state })
```

A third argment provides extra functionality, allowing us to call a function before updating the table value, which can
be used when we need to change some other values, for example when enabling or disabling a class.

### ComputedState

ComputedState takes an existing state and a function which will convert the value of one state to a new one. We can use
this to ensure that a state always stays dependent on another state.
Loading

0 comments on commit d28421b

Please sign in to comment.