Weekly Game Jam #264, Day 1
Day 1 of my devlog for the 264th Weekly Game Jam, with the theme "Many Islands".
By: TheHans255
7/28/2022
I'm participating in the 264th Weekly Game Jam, a recurring game jam on itch.io. It's been a few months since I last did a game jam (Ludum Dare 50 last April), and I've been itching to do another one, especially since I've been wanting to try using the Godot game engine on a complete project from scratch. As I go, I'm going to be keeping a devlog to document my experiences, so here's day 1!
Theme Brainstorming
The theme this week is "Many Islands". A few ideas bounced around in my head at that point, and the one that my brain quickly settled on was the idea that there is a swarm of islands that are alive and migrating or swarming, and you have to navigate them.
Thinking about how to expand this, I pulled out my note-taking app and wrote down more ideas as they came, including ideas that may not be attached to the "living island" idea:
- The islands are alive. They swarm and migrate, often quite quickly.
- The islands are large enough for a handful of people to live on them, complete with the plants and other resources they need to survive.
- Perhaps those people are instead sapient animals, or even just animals
- There should be islands large enough to home at least a few families, to facilitate development of culture
- Some islands are volcanic and cause problems for other islands nearby
- The islands are close enough that you can swim between them (of course, if the islands are alive, then which islands are immediately accessible by swimming will change)
- Some islands hold treasure
- You are an imperialist conquerer looking for treasure and the islanders will protest your actions and fight back
- You might have a boat for traveling between islands that are farther apart.
- You are sent on a hunt for treasure throughout the islands during a massive swarm
- Idea 1: The islands move more or less randomly and you need to find the most treasure in that time
- Idea 2: The islands move in a seeded or handcrafted pattern, and you need to find the best route for treasure. This could also lead to a game where you can only take a few items of treasure with you and have to optimize for the best one.
- Islands can be connected by ropes or bridges. The ropes do affect island movement and migration to some extent, but will eventually break apart if stretched too far.
- A simple zipline (perhaps a rubber chicken with a pulley in the middle) can be used to traverse these ropes
- Larger bodies of water might have sharks or large fish in them that eat you
- Your quest takes place over multiple days and nights
- You need to reach an escape boat somewhere in the archipelago.
- Maybe the islanders have eccentric personalities and worlds, like the year characters in Rudolph's Shiny New Year
- Both you and the escape boat have signal flares that you launch occasionally to find each other
- The escape boat launches their flare at set times during the game
- You can launch your flares whenever you want, but they are a limited resource. You will need other tools for signaling if you run out.
- The escape boat will leave without you if you go too long without any signals. Signals can be used to extend the amount of time you have on the island and also direct the boat to go toward your location. (Editor's note: I also envisioned a Majora's Mask style timer to go along with this.
- The water is controlled by a simple particle system of waves going multiple directions
- The waves of you swimming are a texture around you
- The water is controlled by a dynamic particle system of peturbed waves
- You are a real estate broker trying to sell the islands to people
Eventually, I settled on a main idea with these parameters:
- You have crash landed on one of the islands and are trying to reach an escape ship. You have 3 days until the ship leaves.
- The ship remains docked on one island and sends out a signal flare at sunrise and sunset each day.
- Islands are small and clustered. You can cross to other islands either by swimming through the water or ziplining between islands.
Engine Choice
For my game engine, I decided to use Godot. Godot is an open-source game engine with a node-based visual editor system in the vein of Unity Engine or Unreal Engine. I first took interest in this engine a few years ago as a potential outlet for Rust as a scripting language, and eventually came to appreciate its other features, such as its OOP focus on Node objects for structure and behaviors, its built-in Python-like GDScript language, and its strong support for 2D games alongside its 3D capabilities. There were a few other engines I've used to make jam games in the past, including PICO-8 and LÖVE, but I knew that this traversing islands idea would likely need to be a 3D game, which these engines don't support as well. Plus it's free and open-source, and I use a lot of FOSS stuff on my own systems already, so why not?
Procedural Water
Since this game would involve traversing islands, one of the first things I knew I needed was some decent water to separate them. This ended up becoming a large focus of today, and while I'm not quite proud of everything I did on it, I think a lot of it will still be useful in the final product.
(EDIT: I ended up replacing this shader with a recreation of the water from The Legend of Zelda: The Wind Waker, courtesy of the lovely Nekotoarts. My original shader is left up for posterity - I would recommend against using it!)
The core of my ocean was creating a vertex shader that automatically applied a series of ocean waves. Adding together a series of simple sine waves can be used to create interesting ocean patterns. The Godot GLSL shader code for that looks something like this:
// the current game time in seconds - update this each frame
uniform float time;
// store the wave height to use in fragment shader calculations varying float wave_y_result;
// Calculate height difference for one wave
float wave_y_one_wave(vec2 pos, vec2 start, vec2 speed, float period, float height) {
mat2 rotation = mat2(
normalize(vec2(speed.x, speed.y)),
normalize(vec2(-speed.y, speed.x)));
vec2 relative_pos = rotation * (pos - start);
vec2 relative_speed = rotation * speed;
return sin(relative_pos.y / period + relative_speed.y * time) * height;
}
// Calculate height difference for all waves we intend to use
float wave_y(vec2 pos) {
return wave_y_one_wave(pos, vec2(0.0,0.0), vec2(0.0,1.0), 10.0, 0.8)
+ wave_y_one_wave(pos, vec2(0.0,0.5), vec2(0.7,0.7), 3.0, 0.5)
+ /* and so on - I used 4 of these */
}
// Main vertex shader routine
void vertex() {
vec3 world_vertex = (WORLD_MATRIX * vec4(VERTEX, 1.0)).xyz;
wave_y_result = wave_y(world_vertex.xz);
VERTEX.y += wave_y_result;
}
This worked pretty well, but I needed material to make it look a little more water-like. I struggled with getting a more realistic look, so what I settled on instead was a white colored band to evoke a feeling similar to the toon-shaded water in The Legend of Zelda: The Wind Waker:
varying float band_y;
// Modified vertex shader from above
void vertex() {
// ... previous code ...
band_y = wave_y_one_wave(pos, vec2(0.0,0.0), vec2(0.0,1.0), 10.0, 0.4) + 0.4; }
// Fragment shader body void fragment() {
float max_y = // sum of all wave heights, for proper scaling
if (abs(wave_y_result - band_y) < max_y * 0.01) {
// draw the white, opaque band
ALBEDO = vec3(1.0,1.0,1.0);
ALPHA = 1.0;
} else {
// draw the blue, transparent ocean
ALBEDO = vec3(0.2,0.2,0.8);
ALPHA = 0.5;
}
}
I think there's still more improvement that can be done on this design, but I think it works pretty well for conveying the idea of water so far.
Automatic Water Tiling
Perhaps more important today than the design of the water itself was having a method to extend it across the game world. I knew that I would not be able to get away with just a single ocean mesh, since that would cause me to process thousands of vertices that I didn't need. I also didn't want to tile many smaller ocean objects in my scene at the start, since the game engine would still have to do the work of tiling these and it wasn't clear where the scene would end. Therefore, I create a dynamic tiling system that would load in and delete ocean mesh instances as needed.
Since the code is relatively short and displays many of Godot's signature features, I'll include the whole snippet:
extends Spatial
// Variables made visible to the editor. Variables don't need to be exported
// to be accessed by code, but it's good practice to include them.
// This one in particular has "setget", which guards variable access with "set" and "get" methods
export var center_point: Vector3 = Vector3.ZERO setget set_center_point, get_center_point
export var render_distance: float = 200
export var time = 0
// Constant for the ocean's size - this one is taken from knowledge of the Ocean scene's parameters
const OCEAN_SIZE = 50
// Our ocean scene. We will be loading and instancing from this
// to get our new Ocean chunks
var ocean_scene: PackedScene
// A dictionary keeping track of which oceans are where so that
// we can delete them later
var oceans = {}
// Called once when the attached object (in this case, a base Spatial node) is loaded into the scene tree
func _ready():
ocean_scene = load("res://objects/Ocean.tscn")
add_necessary_oceans()
// Update our running total time as it passes
func _process(delta):
time += delta
// Adds a single ocean tile by adding it to our dictionary
// and to the scene tree
func add_ocean_tile(x: int, z: int):
var index = String(x) + "_" + String(z)
if oceans.has(index):
return
var ocean_node: MeshInstance = ocean_scene.instance()
ocean_node.translate(Vector3(x * OCEAN_SIZE, 0, z * OCEAN_SIZE))
ocean_node.time = time
oceans[index] = ocean_node
add_child(ocean_node)
// Remove all of the oceans that are too far away from our center point
func clean_oceans():
for k in oceans:
var key_parts = k.split("_")
var x = float(key_parts[0]) * OCEAN_SIZE - center_point.x
var z = float(key_parts[1]) * OCEAN_SIZE - center_point.z
if (x * x + z * z) >= (render_distance * render_distance):
var ocean_node: MeshInstance = oceans[k]
oceans.erase(k)
ocean_node.queue_free()
pass
// Add all of the oceans that would need to be seen by our center point
func add_necessary_oceans():
var x_index_min = int(floor((center_point.x - render_distance) / OCEAN_SIZE))
var x_index_max = x_index_min + int(floor(render_distance * 2 / OCEAN_SIZE))
var z_index_min = int(floor((center_point.z - render_distance) / OCEAN_SIZE))
var z_index_max = z_index_min + int(floor(render_distance * 2 / OCEAN_SIZE))
for x_index in range(x_index_min, x_index_max):
for z_index in range(z_index_min, z_index_max):
var index = String(x_index) + "_" + String(z_index)
if not oceans.has(index):
var x = x_index * OCEAN_SIZE - center_point.x
var z = z_index * OCEAN_SIZE - center_point.z
if (x * x + z * z) < (render_distance * render_distance):
add_ocean_tile(x_index, z_index)
// Function for setting the center point. This function allows us to adjust the
// currently active oceans whenever this center point changes.
func set_center_point(value):
// Note that the setget functions do _not_ trigger if you edit the associated variable
// from inside the script without using the "self" keyword. This allows you to access
// the protected variables without creating an infinite loop.
center_point = value
clean_oceans()
add_necessary_oceans()
// Function for getting the center point
func get_center_point():
return center_point
}
To use this, I have a small script in the top level of my test scene that reads the position of the player and copies that to the tiler's center_point
every second (with the help of a Timer
node). As my player moves around (which I implement later), old, faraway copies of the ocean disappear and new ones take their place.
If needed, this system could potentially be extended to create varying levels of detail, where ocean tiles that are farther away have fewer vertices and use simpler rendering effects.
Islands
My current islands are rather simple - effectively, they are cylinders with wider bases that extend into the ocean. Since I haven't gone scrounging for or created any assets yet, I don't even have any trees or anything to put on these islands, much less any interesting land features. I have, however, established that the islands are KinematicBody
nodes, which will allow them to migrate as planned.
Another thing I have going as far as rendering is concerned is a lower section of the island that extends below the ocean, in order to create a more natural slope as you approach the island. To simulate the effect of light dissipating as it goes further into the ocean, I use the "Distance Fade" effect on that portion's SpatialMaterial
to cause them to disappear as you move away from them.
Character Controller
Finally was the character controller, so that I could move around my test level and observe things. This ended up being a pretty standard first-person character controller, with a KinematicBody
, capsule collision box, and WASD/mouse controls. A resource that turned out to be very helpful here was Godot's official voxel game demo, which included a similar FPS character controller.
Here, I also needed to add solid collision to the ocean tiles, so that I could walk across them to the other islands.
And that was Day 1! Over the next few days, I have a few avenues for how to proceed. Some of these are focused on building out more of the game's core functionality, others are focused on juicing up the game to make it more fun to play and work on:
- Clean up the water. Don't spend too much time on this rabbit hole, but do change it to make it more unobtrusive.
- Add true buoyancy to the water to replace the solid platform underneath. Make the wave parameters accessible to the GDScript code and aim to have any characters that enter the water float up to wherever the surface currently is.
- Make the islands more interesting. Find some palm trees, rocks, etc., make some stuff in Blender.
- Make the islands migrate
- Add the escape boat, docked to one island at the end. Complete the game when you approach it.
- Add a day/night cycle and a 3 day time limit.