TheHans255.com

Weekly Game Jam #264, Day 1

by: TheHans255

July 29, 2022

Read Day 2 Here

My workspace at the end of day 1

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:

Eventually, I settled on a main idea with these parameters:

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?

A sample of the Godot 3D editor

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.

The distance fade settings for the bottom half of the island

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:


Copyright © 2022-2023, TheHans255. All rights reserved.