Nulaxia: Character and World!
I’ve been working a bit on a game concept I’ve had for a while. Here’s some out-of-context progress on it!
Character
Nulaxia takes place on an alien planet. So, we need aliens!
- Walking creature with wireframe.
- Creature running along a predefined path.
- Player-controlled creature (with debugging lines) interacting with a second creature.
As a child, I loved those Petz games. You know, by PF Magic? It was a series of virtual pet games from the 1990s and early 2000s. More recently, I rediscovered those games; oh, the nostalgia! I was impressed to learn, though, that the animals in those games were drawn with circles. Just circles. The paws, tail, head, ears…they’re all just circles! That certainly explains the wide range of motions those cats and dogs – sorry, Catz and Dogz – could do.
And I wanted to try that! I decided to experiment with it within Nulaxia since I really wanted to see the alien creatures in it move in that natural, fluid way. I planned to set it up like a 2D puppet rig. Each circle would shape the body, and the elasticity between those circles – how quickly they move toward their parent joint – would serve as bones in a way. Paw placement would need to be automatic; the player should be able to move their character, and the paws should place themselves accordingly.
Fortunately, putting this into practice was actually kinda easy. Sure, the paws were a little difficult, but I managed to put together the above in just a couple hours. And, to my greater surprise, despite the number of circles and the functions each one calls every step, the creatures take very few resources; PICO-8 has no problem controlling multiple instances of a creature.
World Generation
Pico-8’s processing memory is harsh, and noise generation is resource-heavy. Fortunately, the game doesn’t have to show the entire planet, at least not accurately. What’s important is A) that I can get the noise value of adjacent screens, and B) that the noise texture – and thus the world generation – tiles horizontally.
- Linear mapping to 2D circle.
- Things aren’t working as planned…
- Longitudinal-latitudinal mapping to 2D circle with day/night shading.
- Demonstration of movement across longitudes and latitudes. Squares represent sampled color from circle (top), map (middle), and noise (bottom).
Software like After Effects makes tileable noise easy because it does all the magic for you. But PICO-8 doesn’t have this magic. So I had to code it myself.
Well, I didn’t code the noise algorithm myself; Perlin is open-sourced and available in Lua. With a few tweaks to allow for things like random seeds, my cart was ready to make some Perlin noise!
But what about tiling the texture? Perlin noise, and any other noise generation algorithm for that matter, generates noise infinitely in each axes available. So, it’s not as simple as cutting out a square and tiling it. I had to do some research as a result, and…what I found was mind-blowing! It’s genius, really! Basically, the idea is to use 3D noise – noise that changes over three axes (X, Y, and Z). Each horizontal line in the texture itself comes from a circle on the X and Y axes! For example, while V = 0, every value of U equals a spot on the circumference of that circle. Like, if the map is 180 pixels long, and if the radius of the circle on the noise map is 10 pixels, then (U,V) value (0,0) will be the point 10 pixels in the direction of 0° from the origin, (1,0), will be 10 pixels at 2°, (2,0) will be at 4°, and so forth until (179,0) being a point 10 pixels from the origin in the direction of 358°. The last pixel is thus near the first pixel on the noise map, as close as any other pixel. Therefore, that row tiles!
And what about the V axis? Well, that’s a lot easier; just increase the Z axis value of the Perlin noise for each V value, and repeat! In the end, you’ll have cut a hollow cylinder out of the 3D noise and unwrapped it into a rectangle. And therefore, it will tile!
Here’s how it works in Lua within PICO-8:
--[[
Assuming the following:
perlin(x,y,z) is your 3D Perlin function.
horizontal_scalar is how "stretched" the U axis should appear.
vertical_scalar is how "stretched" the V axis should appear.
]]
function get_noise_at(u, v)
local x = sin(u) * horizontal_scalar
local y = cos(u) * horizontal_scalar
local z = v * vertical_scalar
return perlin(x, y, z)
end
So now we have a map. Great! But we need to move a character around in this world. My first thought was, of course, to just have a world map, where U is the X axis and V is the Y axis. However, this is a planet: a sphere. So, moving horizontally on the equator would appear significantly slower than moving horizontally near the poles! So, it would make more sense to dismiss the Cartesian model and go for a geographical approach, where U is the longitude and V is the latitude.
In-game, the world will appear as regions, currently about 64×64 pixels. PICO-8’s screen is 128×128 pixels, and the game is top-down, so the screen may contain up to 9 of these regions at a time. Each region will be a (U,V) value, each one an integer. Regions to the left and right will differ in the U value, while regions above and below will differ in the V value. While PICO-8 certainly can’t handle processing a huge map of many, many noise values (at least without caching these noise values), it can easily handle generating 9 noise values at a time and more. Therefore, the issue with performance, and any possible issue with translating between UV / longitudinal-latitudinal values and Cartesian XY values, are both solved.