The Mast 3D


Play it here: TurboWarp | Scratch | Standalone (TW packager)


A textured 3D game set on a fictional planet. Explore highly detailed environments and structures and uncover the events that took place. Inspired by Infra and Rain World, among other things.

Controls can be set inside the project, by default it is WASD and mouse click/drag.


General technical details


Textures

This project uses a custom text-based image format developed by myself, with help from Arctenik. This was necessary due to the fact that the textures take up a lot more space than what is allowed to be stored in a Scratch project's project.json, which has a limit of 5MB, including scripts, vars, and anything else necessary to run the project. Costumes and audio are stored separately and have their own limit of 10MB each, no limit on total count. This image format compresses 24-bit RGB images which you can read about in more detail in the linked project description.

The project size limit leaves few options for storing the needed textures. Even with compression, the full-res textures do not fit. I provide a low-res version in the project that can be loaded at any time but the full-res textures must either be scanned or pasted in via a text input box. The pipeline for importing full-res textures can be seen below. Half-res textures are the exact same except there is no branch for image scanning as it doesn't need it.

Additionally it must be noted that an exception is made for the starting Pod region. The player doesn't get to choose the texture quality for it, the full-res texture is automatically loaded from a separate variable storing it.

A compressed texture by itself is pretty useless, it's just a stream of pixel colours. To make it usable, "metadata" is included and this is handled in the above diagram too. The format for the actual string accepted by the project is (spaces separate values):

[texture_name] [number_of_pixels_total] [texture_width] [texture_height] [checksum] [compresed_data]

This repeats for every texture needed.

The checksum is used to ensure texture data is intact. It is just a sum of the compressed data stream with each character being counted using its index in a list (matches the ordering of ASCII except with different indices, ! is 1).

Here's one of the textures in a format that can be seen: region_start_off.png

If you want the texture data in text form, go here: The Mast Textures


Models

The world was created in Blender. I have a single .blend file storing everything and this gets exported to .obj which the project is able to read. Inside the project is a list called WORLD.obj, this is the file with lines as seprate items.

There is a very important naming system being used to specify what each mesh is responsible for:

[region name],[FL/VZ/DY],[room or object subname(if not FL)],[BB (if not FL)]
FL is floor
VZ is static visible geometry
DY is dynamic geometry (things that can be moved such as doors)
BB is bounding box and is used by static geometry for only rendering when unoccluded. Dynamic geometry may also use it but I don't bother.

Here are examples:

start_off,VZ,corridor
walkways,VZ,lower,BB
surface,DY,surface_keycard
drill,VZ
track,FL

There is only 1 floor per region, which is fine as it's not slow and it makes the collision wall generation (for the player's movement) simple. Walls are generated by finding exposed edges (edges that are not shared by 2 adjacent triangles). A line segment is added. Actually, the walls are geometrically stadia extruded along the Z axis.

Regions are split into rooms for improved performance. Rooms are separate objects. The default main room which is always visible will not have a subname, it will just be [region name]_VZ. Any other room must be accompanied by a bounding box to be visible. The bounding box is calculated by the bounds of the tris of the object and can be of whatever you want. I use rectangular prisms to make visualisation easier. Dynamic geometry does not need a bounding box, it will always be visible. A bounding box of size infinity is created automatically for the main room and dynamic geometry.


Player movement

This game uses a fixed update rate of 120Hz for player movement. This is to ensure that collisions occur correctly and are deterministic. The timer also is updated with this. Each game frame, a discrete number of movement ticks are run. At 60 FPS, that would be twice. Limitations are in place just in case unexpected data is found, at most 20 ticks can occur each frame (equivalent to 6 FPS) and past that, the player will move slower.

The player is the camera. There is no differentiation.

Movement occurs along the XY plane and Z axis separately. The player moves horizontally as needed and collisions occur in 2D on line segments. If the player is above a floor triangle, the camera will be set so it is exactly 1.65m above it (the eye height of the player).


Debugging

Enable dev (developer) mode. There is a variable in the project called ! dev, which should be set to 1 to enable. By default it is set based on the username (in the main sprite). Dev mode provides access to a bunch of developer tools and cheats, including a command system. Note that it is primarily meant for the developer of the project (of course) and does not have the same polish the playable project has. The command input box can be opened with the / key. The command syntax is [command name] [params]. The full list of commands can be seen inside the project, in the relevant sprite.

I highly recommend using TurboWarp or other scratch extensions to make use of the "debug blocks" in this project (the log, warn and error blocks). If an error occurs, it will likely be logged and give an explanation of what's known or assumed. Note that you should be using dev mode as some require it to be triggered.