Understanding hot-reloading
As you program your game, you want to see changes live. While Vectarine supports hot-reloading, it’s important to structure your code appropriately to best take advantage of it. Let’s see how.
Resources
Section titled “Resources”Vectarine thinks in terms of resources. A resource is basically a file, it can be code, an image, sound, etc. Each resource is hot-reloaded individually.
When you edit a file, Vectarine hot-reloads it, meaning it rereads its content and updates the corresponding resource.
Each resource can depend on other resources, for example a Luau script can draw an image. Because the script holds a pointer to the image resource, when the image is hot-reloaded, the script will use the new version of the image without needing to be reloaded itself.
Scripts
Section titled “Scripts”A script resource is a Luau table that is returned by the script file.
Inside a module, you can use loader.init() to get the content of the module itself, and require(path) to get the content from other scripts.
You need to keep this in mind, when dealing with scripts requiring other scripts.
Let’s consider the following example:
const debug = require("@vectarine/debug")const other = require("./other.luau")
local value = other.getPopulation()
function Update() debug.fprint("Value is ", value)endconst loader = require("@vectarine/loader")const module = loader.init() or {}function module.getPopulation() return 3endreturn moduleIn this example:
- When the game is first loaded,
otheris not ready, so Vectarine pauses execution ofmain.luauand starts loadingother.luau. - Once
other.luauis loaded,main.luauis resumed andother.getPopulation()is called, returning3which is printed in the console. - If we edit
other.luauto replace the3by a6, main won’t be reloaded, so3will still be printed. - If we edit
main.luauagain,other.getPopulationwill be called once again6will be printed in the console.
Now let’s say we update other.luau to the following and reload the whole project (with
const loader = require("@vectarine/loader")const module = loader.init() or {}
local function count(self) return self.humans + self.aliensend
function module.getPopulation() local data = { humans = 3, aliens = 5, count = count } return dataendreturn moduleAfter hot-reloading main.luau, it will display {human = 3, aliens = 5, count = function}.
Now, let’s say we edit the count function in getPopulation in other.luau. Nothing will change. This is because the count function is located in the data table.
When other.luau is hot-reloaded, the value variable in main.luau stays the same and still uses the reference to the old count function.
This design where we initialize an object with functions in its properties in other.luau and keep it in main.luau is not hot-reloading friendly.
Now, let’s say we change main.luau to the following:
const other = require("./other")const debug = require("@vectarine/debug")
function Update() local population = other.getPopulation() debug.fprint("Population is ", population:count())endHow, when changing count in other.luau, the behavior is instantly reflect in main.luau.
However, we are creating a new population object every frame which is not ideal.
An object-oriented approach
Section titled “An object-oriented approach”The best solution is to rethink the structure of the code:
const loader = require("@vectarine/loader")-- The init function returns the content of the module from the past reload. This allows to keep the same table and merge the new content with the old one.-- It's return type is marked as nil for auto-completion reasons.const module = loader.init() or {}
-- We put the functions associated with the population type inside the module.module.count = function(self: Population) return self.humans + self.aliensend
-- This function is the "constructor" for our Population typefunction module.makePopulation(): Population const data = { humans = 3, aliens = 5, } -- The metatable is set to the module itself, so that population:count() will always use the latest version of the count function, even after hot-reloading. return setmetatable(data, { __index = module })end
-- Our type Population has 2 fields: humans and aliens,-- as well as all the methods defined in the module (like count).export type Population = typeof(setmetatable( {} :: { humans: number, aliens: number, }, { __index = module }))
return moduleconst other = require("./other.luau")const debug = require("@vectarine/debug")
local population = other.makePopulation()
function Update() debug.fprint("Population is ", population:count())endWith this design, when you edit the count function in other.luau, main.luau calls the latest version without you needing to reload it or create a new population object.
What is happening here is that when you hot-reload other.luau, module is not recreated. The same table is kept and its old content is merged with the new one.
This content is what gets returned by module.init()
This means that main.luau has a reference to the newest version of the count function through the metatable of population and hot-reloading works as expected!
Using loader.init() has other benefits, for example you can now store the state of your game in the module and it will be preserved across hot-reloads.
const loader = require("@vectarine/loader")const module = loader.init() or {}
module.reloadCount = (module.reloadCount or 0) + 1
-- Do not reset the enemies table if it already exists!module.enemies = module.enemies or {}
return moduleHere, module.reloadCount will be incremented every time other.luau is hot-reloaded, and the enemies table will be initialized once and preserved across hot-reloads.
This is because loader.init() returns the same table across hot-reloads, so we can use it to store state that we want to persist.
Lessons for organizing code
Section titled “Lessons for organizing code”We can thus learn the following principles for organizing code in a hot-reloading friendly way:
- Except for
main.luau, avoid putting code in the global scope. Instead, put it inside functions. You don’t know when code in the global scope will be executed which can lead to issues with loading order. - Use function in modules and avoid storing functions in objects
- Define one main type per module with associated functions for working with that type in the module.
Unlike scripts, some resources like images and sound are not immediately ready when loaded. You can use Graphics.drawSplashScreenIfNeeded to display a splash screen while waiting for resources to be ready.
const loader = require("@vectarine/loader")const graphics = require("@vectarine/graphics")
-- load your scriptslocal image = loader.loadImage("textures/image.png")local sound = loader.loadAudio("audio/sound.mp3")
function Once() _G.isOnceCalled = true -- Your init code here. You can be sure that the image and sound are now ready to be used.end
function Update(deltaTime: number) if graphics.drawSplashScreenIfNeeded({ image, sound, }) then return end if not _G.isOnceCalled then Once() end
-- Your game loop hereendBecause Update is the entry point of your game, all code that gets called from it will access resources that are already loaded.
Inside drawSplashScreenIfNeeded, you can put images, sound, fonts and even scripts resources (coming from the loadScript function)!
