The Mast


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


Main loop

At the core of the project is the main loop. A single forever loop controls the entire project apart from the audio which has its own loops to prevent interruption. This main loop sets essential variables and then broadcasts to all the other sprites to run scripts in the desired order. Note that none of these broadcasts are broadcast and wait. It's not needed as all the scripts will complete within the frame.


Background

The sky is just a stamped costume that moves vertically. For the different altitudes or regions there are different backgrounds. The sun is just a 3D point very far away with a costume stamped at its location on the screen. The mast itself is rendered with pen lines. I originally tried with stamps but it was of poor quality - too much aliasing and no distortion due to perspective. The script responsible is hacked together to draw something that looks "good enough" for both vanilla Scratch and TurboWarp.


Geometry

When a region is loaded, all the geometry for it is placed in some lists. There is a list for the vertex coordinates, and another that was added later for storing extra data associated with each vertex, which ended up being a boolean for if it is dynamic and the subregion name as a string. Triangles are stored in a separate list and contain the 3 vertex indices, the surface normal for backface culling, and the 3 pairs of uv coordinates for each vertex. An extra item is used as a separator for aid in debugging. In total that is 10 items per triangle.
During gameplay, this geometry data is sent to the processor sprite every frame where it gets transformed and projected onto the screen. Any geometry where the camera is outside of its bounding box is skipped. Dynamic geometry is also processed and movements are made through simple vertex displacements along a single direction before the screen transforms. This type of geometry has backface culling disabled for simplicity.
Rendering is handled by a different sprite. Here z-clipping is performed using a quite large script and finally the processed triangles are rendered using Bambozzle's textured triangle filler.


Textures

This project uses TextImage to store its textures. 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.


Game states

There are a lot of values that need to be kept track of. Every elevator position, every button state. All states are stored within a list. Another list simultaneously stores the name of the state, so they are really key-value pairs. For most purposes the state is found using the item # block, which is inefficient but a compromise to keep the code readable. As the order of the states never change, an easy improvement would be to directly access the values using the correct index. The dynamic geometry is driven by some of these game states which can be interpolated within a set range. Some more complex animations make use of sinusoidal interpolation to make it much smoother, although internally it is still linear. The sinusoidal interpolation is mapped from the linear values.


Player physics

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).

There is only 1 floor per region, which is fine as it's not that slow and it makes the collision wall generation simple. Walls are generated by finding exposed edges (edges that are not shared by 2 adjacent triangles). A line segment is added there. Walls are given a height for where their collision is effective, which is a range from 1 metre below the bottom of the wall and 1 metre above the highest point of the wall.


Tags

Placed in the world are what I call "tags". Tags can do 3 things, display text such as for signs, open a document to view, or run functions when triggered such as a button or entry into a new region. Text display is the simplest, it is just specified in the custom block. Document is similarly simple, just specify the document name. Functions are a bit more complicated. You can specify a game state to set using special syntax. Any other function name will trigger a different script with hard-coded functionality. The game state syntax is just to lessen the amount of code that is needed as it is a common thing that needs to be run.


Documents

Documents are pretty simple, just an image or set of images displayed on the screen with text. The images used in the documents were created in Blender. The keypad found in the facility region is also technically a document but programmed with additional functionality. Same as the computer terminal at the end of the game.


Audio

Sounds in Scratch is a pain to deal with. It's very limiting and there are major difficulties such as sound being unable to seamlessly loop and some sound blocks causing yields. This is what I ended up settling with but I am still not satisfied. The whole system is a bit rushed too.

Sound emitters can be placed in the world to create looping sounds. They can be made to pan based on their screen space position which is a flawed technique. In hologram render mode the sound emitters can be viewed.

The player steps are simple. There are variables storing the last foot position, if the player moves a certain distance from it, place a new foot there and play a step sound. In some places, an alternate metallic step sound is played, which is controlled by hard-coded location checks.


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.