SolarHorizon

Top To Bottom: Fully Managed Rojo

Working outside of Roblox Studio is a concept I’ve seen a lot of Roblox developers struggle with, even those who are actively using Rojo. Some don’t see the benefit, and many just don’t understand how it works. I’m going show you how I’d go about setting up a new fully managed project from scratch, and along the way, I’ll try to explain how your next project could benefit from being fully managed. It’s important to note that my way isn’t the only way. I might do something differently from how you or someone else would do it, and that’s okay. We’re all making different games with different requirements.

The easiest way to set up a fully managed Rojo project is to start fresh. It’s not impossible to convert an existing project, but the level of difficulty in doing so is entirely dependent on what the project is, how the codebase is structured, and whether or not you’re already using Rojo. If you are already using Rojo with a partially manged project, it should be pretty straightforward. It’s up to you to determine whether or not the time and effort to convert an existing project is worth it. Will it save you time in the long run? If you’re working with a team, are they already familiar with Rojo and any other tools you might use alongside it? If not, are they willing to learn? How long will that take?

Laying The Foundation

Before we get started, there are some prerequisites you should have if you intend to follow along. You’ll need:

For this guide, we’re going to create a brand new project.

$ mkdir fully-managed
$ cd fully-managed

Once you’ve done that, create a Rokit manifest.

$ rokit init

You should notice that a new rokit.toml file was created. This is the Rokit manifest. It’ll keep track of each tool you’ve installed while working on this project as well as the specific version of it that you are using.

It’s important to do this because it’ll keep track of any tools that are required to work on the project. When working with a team, it’s crucial that everyone is using the same version of the same tool. Sometimes there are inconsistencies between different versions of software that can cause big problems. You’ll avoid that entirely by standardizing the version your project uses with Rokit.

Now we’re going to install a few tools. We’ll start with Rojo.

$ rokit add rojo-rbx/rojo

Once Rojo is installed, create a new Rojo project. You can do this manually, or you can let Rojo do it for you with the init command. I usually take the auto generated project the init command gives me and modify it to fit my needs. For projects that are very similar to each other, I might use a template if I’ve made one. For now, let’s stick with the one Rojo gives us.

$ rojo init

There should be a few more files now. Most notably, Rojo has initialized a Git repository for you and created a default.project.json file. The file should include a baseplate under Workspace and some lighting settings. We’re going to remove those since we’ll be replacing them with our own later.

After deleting those sections, your project file should look something like this:

default.project.json
{
  "name": "fully-managed",
  "tree": {
    "$className": "DataModel",
    
    "ReplicatedStorage": {
      "Shared": {
        "$path": "src/shared"
      }
    },
    
    "ServerScriptService": {
      "Server": {
        "$path": "src/server"
      }
    },
    
    "StarterPlayer": {
      "StarterPlayerScripts": {
        "Client": {
          "$path": "src/client"
        }
      }
    },
    
    "SoundService": {
      "$properties": {
        "RespectFilteringEnabled": true
      }
    }
  }
}

Level 1: Managing Your Maps

A common way to handle collaboration between builders and programmers when fully managed is to set up a dedicated game on Roblox for building. This gives builders their own environment where they’re free to work as they normally would, and it makes it easy for us to find the most up to date version of the map. Then, you can download the most recent version anytime you need it.

That sounds pretty tedious, though, right? You’d need to open the game in studio every time you wanted to update your map. Luckily, we’ve got tools like Lune to make this a lot easier. Install it with rokit:

$ rokit add lune-org/lune

Lune needs a little bit of setup if you want your editor to understand what you’re doing. It’s easy, just run this command:

$ lune setup

Lune will let us write Luau scripts to automate parts of our workflow. It’s got a built in Roblox library that allows for reading and modifying place and model files. It’s also able to send HTTP requests, making it the perfect tool for us to download a place file from the Roblox website and use it in a way that’s nearly identical to using instances in Roblox. I’ll show off some of those capabilities later on. If you want to learn how to use Lune on your own, check out the docs.

Now that we’ve got Lune installed, create a new directory named lune in your project. Once you’ve done that, create a new file inside of it named download-place.luau and let’s write a script to download our place.

lune/download-place.luau
local net = require("@lune/net")
local roblox = require("@lune/roblox")
local fs = require("@lune/fs")

local function downloadPlaceAsset(placeId)
	local cookie = roblox.getAuthCookie()
	assert(cookie, "Failed to get auth cookie")

	local result = net.request({
		url = "https://assetdelivery.roblox.com/v1/asset/",
		headers = {
			Cookie = cookie,
		},
		query = {
			id = tostring(placeId),
		},
	})

	assert(result.ok, result.body)

	return result.body
end

local content = downloadPlaceAsset(123) -- replace with your own place id!
fs.writeFile("./game.rbxl", content)

This script will download the place using the given place ID and then save it in your current directory as game.rbxl. Unfortunately Roblox doesn’t let us download places using the newer Open Cloud API, so you’ll need to use the auth cookie of a Roblox account with access to the place to download it. Lune’s Roblox library comes with a helper function that can handle this for us, but you’ll have to have logged into Roblox Studio once before using it for it to work.

You can run the script using the lune run command:

$ lune run download-place

This is a good foundation to build on top of to really get started with our fully managed workflow. You could take this a step further and modify the game in some way before saving it to your filesystem. You could also break the game down into smaller pieces and save them as models. We’re going to do that now because saving parts of our game as models will allow us to use them in our Rojo project.

We’ll reuse the downloadPlaceAsset function we wrote before for this. It might be helpful to break that function out into its own file if you think you might be using it often in other scripts you write. I like to put reusable code in a directory named lib inside of my lune directory. I’m going to write this script with the assumption that you’ve done that.

To get our map working, we’ll want probably to save the Workspace of our downloaded place file. We should also save Lighting in case our builders have made any lighting adjustments or added any post-processing effects. Most games are more than just a map, though. They’ll have models that are cloned, moved around, or given some behavior while the game is running, like equippable items or enemy NPCs. These things don’t always exist as part of our map, so we’ll store these models in a folder found at ReplicatedStorage.Assets.Models, and we’ll assume our builders have put all of these models into a folder named Assets in the place we downloaded from Roblox.

lune/import-assets.luau
local downloadPlaceAsset = require("./lib/downloadPlaceAsset")
local roblox = require("@lune/roblox")
local fs = require("@lune/fs")

local content = downloadPlaceAsset(123) -- replace with your own place id!
local game = roblox.deserializePlace(content)

if not fs.isDir("./map") then
	fs.writeDir("./map")
end

if not fs.isDir("./assets") then
	fs.writeDir("./assets")
end

fs.writeFile("./map/Workspace.rbxm", roblox.serializeModel(game.Workspace))
fs.writeFile("./map/Lighting.rbxm", roblox.serializeModel(game.Lighting))
fs.writeFile(`./assets/Models.rbxm`, roblox.serializeModel(game.ReplicatedStorage.Assets))

Just like before, you can run this script with lune run:

$ lune run import-assets

That’s pretty cool, but if you were to try to build and open your Rojo project right now, the first thing you’d probably notice is that your workspace is completely empty. That’s because we still need to add the Workspace, Lighting, and assets from our imported map to our project file.

default.project.json
{
  "name": "fully-managed",
  "tree": {
    "$className": "DataModel",
    
    "ReplicatedStorage": {
      "Shared": {
        "$path": "src/shared"
      },
      "Assets": {
      	"$path": "assets"
      }
    },
    
    "ServerScriptService": {
      "Server": {
        "$path": "src/server"
      }
    },
    
    "StarterPlayer": {
      "StarterPlayerScripts": {
        "Client": {
          "$path": "src/client"
        }
      }
    },
    
    "Workspace": {
    	"$path": "map/Workspace.rbxm"
    },
    
    "Lighting": {
    	"$path": "map/Lighting.rbxm"
    },
    
    "SoundService": {
      "$properties": {
        "RespectFilteringEnabled": true
      }
    }
  }
}

Now let’s take a look at what we just set up in Roblox Studio. Build your project, like so:

$ rojo build -o game.rbxl

If everything was done correctly up to this point, you should see a new game.rbxl file. If you open it in Roblox Studio, Workspace now contains our map, all of our lighting properties are set up, and our models are in ReplicatedStorage where they belong. At this point, your project fits the description of “fully managed”. If you’re content with just having your models, map, and code managed outside of Roblox, you can stop here. We can take it a step further though.

Level 2: Beyond Rojo

Asphalt is a tool that can manage your images, audio files, 3D models (not to be confused Roblox models!), and animations. We’re going to set it up so that all you need to do is drop your files into your assets directory, run a command, and then instantly be able to access all of these assets right in your code.

First, we’ll need to install it:

$ rokit add jackTabsCode/asphalt

Now we need to set it up in our project. Asphalt will ask you a few questions about your project before it’s finished. If you haven’t run the import-assets script, you either need to run it now, or create a directory named assets before proceeding. We’re going to be using that directory now. Make sure to fill in your own UserId or GroupId depending on whether you’re setting up a game owned by your Roblox account or a Roblox group.

$ asphalt init
> Asset source directory assets
> Output directory assets
> Creator Type User
> User ID 8675135
> Output name init
> TypeScript support No
> Style Nested
> Strip file extensions Yes

Since we’ve set up asphalt to process all of the files in our assets directory, we’ll need to open up the asphalt.toml file that was just generated and tell it to ignore a couple file types that we don’t want it to handle:

asphalt.toml
asset_dir = "assets"
exclude_assets = ["*.luau", "*.rbxm"]
write_dir = "assets"

[creator]
type = "user"
id = 8675135

[codegen]
output_name = "init"
typescript = false
style = "nested"
strip_extension = true

This is how I usually configure Asphalt. Some parts of the configuration are personal preference. You can play around with it to see what you like, but the rest of this guide will assume you’ve configured it the same way I have.

We’re not done just yet though. Asphalt needs permission to use our account to be able to upload our assets to Roblox. We can do this by creating an Open Cloud API key for Asphalt to use. First, let’s make a place to put it. Create a .env file in your project, and add it to your .gitignore file:

.gitignore
# Project place file
/fully-managed.rbxlx

# Roblox Studio lock files
/*.rbxlx.lock
/*.rbxl.lock

# keep my secrets secret!
.env

It’s very important that you don’t forget to add it to .gitignore because if you commit it into your git repository, anyone with access to it will be able to read your API key.

Head over to the creator hub so we can make one. Once you’re there, create a new API key. You can name it whatever you want, but make sure to give it access permission to the assets API system, with read and write operations. Most people don’t have a static IP address, so for accepted IP addresses, just put 0.0.0.0/0 to allow all IP addresses.

Once you’ve saved your key, copy it to your clipboard, and create a new variable containing the key in the .env file you made earlier, like so:

.env
# Your API key will be much longer, I've truncated it here.
ASPHALT_API_KEY="kQII9it2b02Uu3HteeE3aKhtqXn..."

Now that we’ve got Asphalt set up, we need to add some assets to our directory! Create a new images directory under assets and put an image file into it. Asphalt supports .png, .jpg., .bmp, .tga, and even .svg files. Once you’ve put your image into the directory, run this command to upload it to Roblox:

$ asphalt sync

If everything is working correctly, that should have uploaded your image and created an init.luau file in your assets directory. If you open that file, it should look like this:

assets/init.luau
return {
    images = {
        image = "rbxassetid://18726181744",
    },
}

If you remember from before, our assets directory gets synced into our Rojo project as ReplicatedStorage.Assets. Now it’s a ModuleScript, and when we require it, we get a big map of all of our asset IDs!

If you build the project and open it in Studio at this point, you might notice that the Assets module contains an empty images folder. If that bothers you, you can add it to the globIgnorePaths property of your Rojo project, or change Asphalt’s output directory to somewhere inside of your src folder. If you do that you may also want to change the output_name under codegen to something other than init. You could also come up with another structure that works better for you.

If you like how I’ve set this project up, you can check it out on GitHub.

Level 3: Infrastructure As Code

I’m not going to go too far into detail in this section because Roblox doesn’t officially support a lot of what it covers, and they have a tendency to break things that they never promised to support, which is fair enough. It’s possible to manage your entire game, including the thumbnail, title, description, game passes, developer products, and even game settings like voice chat or avatar type, using Mantle. This is incredibly useful if you want to spin up multiple instances of the same game to be used for different purposes, i.e. you want to have multiple different testing environments, plus a live game. It really shines when you’ve got a multi-place game which would take a long time to set up manually. This is well beyond the scope of a fully managed Rojo project, though, so if that’s something that sounds interesting to you, take a look at the docs.

Conclusion

Starting a fully managed project consists of a lot of upfront work, but in the long run, could save you a lot of time by cutting out a lot of the monotonous tasks we as Roblox developers have to deal with when making games. If you’re still unsure about it, that’s okay! Personally, I like fully managed Rojo because it allows me to go to any point in my git history and build a complete game. It also allows me to onboard new people to my projects very quickly. They just need to run a couple commands to spin up a new development environment, and we’re not stepping on each other’s toes by syncing everything into a team create session at the same time.