Though Love2D is a 2D engine, I mainly work in 3D using Lua as my main scripting language. Around this time I was trying to experiment with less high-level engines such as Unity and Roblox to instead try to focus on creating my own core systems from scratch. A few months after I heard about Love2D, I found a 3D library for Love2D called "3DreamEngine" that handles a lot of core 3D stuff using OpenGL, such as materials, meshes, shadowmaps, and more. This meant I had easy access to a 3D low-level engine. Finding out about 3DreamEngine allowed me to dive head first into programming low-level systems, which would eventually become a great learning experience.
The main idea behind this project was a simple 3D survival game from a 3rd person perspective using randomly generated terrain. You play as a character that can simply roam around the world.
A camera system that handles rotations.
Procedural terrain generation.
Along with a voxel library for handling world models, such as trees and chunks.
Procedural animations.
A simple physics engine. [Will be covered on another page.]
Used to develop the character controller.
A positional audio system.
Starting off with the Camera System, I decided to stick with 3DreamEngine's implementation, which has set values for the rotation as a vector. These values are set up based on the Δ of the mouse's position between frame updates. This change in rotation might seem too sudden, so using linear interpolation allows for smoothing out the motion.
The procedural terrain generation system is based off of Lua's native perlin noise implementation (math.noise). The terrain algorithm generates a number from 0-1 for each chunk coordinate (every 0.25 units) of the given size of the world. The generated numbers are then varied based on an amplitude (The intensity/height of the chunk) and frequency (The turbulence of the overall terrain) variable that's passed through the noise generator. After this, the chunk is given a material based on it's height, and vegetation is spawned randomly.
As an extension, the voxel library handles all the references and model info for chunks and vegetation models. Every chunk and model that gets created and rendered is stored within an array that get's drawn on the GPU during each frame.
Here's an early version of the project with camera movement and terrain generation:
Considering the low-level implementations I was trying to achieve with this project, creating animations using an animation editor was out of question. An interesting solution that lots of game developers use as an alternative is procedural animation, or animation that's driven mostly by code through math.
Considering the player would be moving around by walking, jumping, and landing, sine waves seemed like a reasonable and common method for creating these movements. Sine waves are mostly used in games for creating smooth oscillations, such as moving platforms, pendulums, and other back and forth movement. Since they allow the use of frequencies and amplitudes, they're highly flexible for creating movements at different speeds and intensities.
The character model in the project is made up of several meshes for each body part, such as the right arm, left arm, left leg, right leg, torso, and head. I ended up manipulating the transformations of these models to offset their rotation and position relative to the torso of the character. During each update call, the rotation of these body-parts are offset using sine waves. Some poses, such as jumping and landing, don't use sine waves and are instead pre-defined offsets. Finally, I used linear interpolation to smoothly move between these poses.
Here's the end result of the character animation:
The audio system was the last element that was created for the project. Love2D has it's own audio system by default, but as usual it doesn't support 3D positional audio. Consequently, I created an audio library that updates each audio source every update call, where the volume of each audio source is determined by the vector between the camera and the position of the source, which is then divided by a given radius.
Here's the final product of the project with all the requirements implemented: