LODESTAR

Showing posts tagged indie game

I’ve been working on a cave generation system and trying to achieve an organic feel. The starting point for my method was inspired by this post.
Parameters:The cave system generator has several parameters.
Seed - integer used to seed the pseudo-random number sequences
SystemSize - how many caves deep and wide the system is
CaveSize - width and height of each cave
MaxWorkers - count at which to deactivate all workers and stop looping
SpawnChance - percent chance to spawn new worker
Threshold - distance from center at which worker movement becomes harder
Layers - how many data arrays to layer into one cave
Create an Array: First I create a two-dimensional integer array to hold the data. For my purposes a 256 x 256 array is large enough. The array is filled with integers that represent the tile type at an (x,y) position. Currently I have a type index for wall, floor, transition up and transition down. I actually use a one-dimensional array and access it like so:
int get(int x, int y) {
    return data[x + width * y];
}

void set(int x, int y, int val) {
    data[x + width * y] = val;
}
Create Workers: Next, I create a worker object, activate it, drop it in the center of the data array and set the value at its location to the floor value. Now I start looping through all active workers. During the loop, each active worker is given a procedurally shuffled set of four directions: N, E, S and W. Each direction is checked in order from first to last; if a wall is encountered in the checked direction, the worker is moved to that location and the value of that location is set to the floor value. If no walls are encountered, the worker is deactivated and removed unless the worker is the last active worker; the last active worker will move, without spawning new workers, until it finds a wall. Each time a worker moves, it has a chance [SpawnChance] to spawn a new worker. Again the parent worker is given a procedurally shuffled set of four directions. Each direction is checked in order; if an unoccupied floor is found in the checked direction, a new worker is spawned in that location and the worker count is incremented. If no unoccupied floor is found, the new worker is not spawned. If the distance from the center to the worker is farther than the [Threshold] parameter, the worker’s movement in the direction away from the center becomes exponentially harder the farther it moves past the threshold. If a worker reaches the edge of the data, it is re-positioned at the center. After the worker count reaches the [MaxWorkers] parameter, all workers are deactivated and the loop exits.
Layer Data: The previous steps are performed [Layers] times and each set of data is layered on top of the last. Wall values are changed to floor values only. At this point, we have something that looks like this:Smoothing: The next step is taking the combined layers and smoothing the result. I split my smoothing algorithms into two functions simpleCleanup() and complexCleanup(). The simple function loops through each wall and checks the surrounding values in opposing pairs; if the N and S values are floor or the E and W values are floor, the wall is set to floor. The complex function is a little more, well, complex. It loops through each wall and if at least three of the NW, NE, SW, and SE values are floor, the wall is set to floor. To achieve the smoothed data you see below, first the simple and complex functions were performed each four times, starting with the simple one and alternating between the two; simple -> complex -> simple -> complex -> etc. Finally two simple passes are performed. This results in the smoothed, organic looking cave below. Resize: After each map has been created and smoothed, the minimum and maximum x and y values are calculated and snapped to a grid based on my game’s x and z chunk size, which is 16 x 16. Then, if the cave’s floor is too close to the edge, that edge is padded with a strip of chunks full of wall values. The map is re-mapped to a new map with the new size.
Transitions: To connect the caves in the system, I first generate a maximum number of transitions for the first cave. This number is proportional to the number of floor tiles in the map. Next, a random number of transitions are generated, not to exceed the maximum number of transitions. A map is then generated and linked for each transition, provided the number of maps at the current depth does not exceed the maximum cave system width parameter. If the map depth equals the maximum cave system depth parameter, no transitions are generated and the cave goes no deeper.
Extra: I perform some extra, game-specific steps. I generate a list of indexed chunks for each map and each chunk object contains data about the 16 x 16 region; data such as how many floor tiles are in the chunk. The image of the caves at the top demonstrates this by coloring each chunk a brighter shade of green the more floor tiles it contains.

I’ve been working on a cave generation system and trying to achieve an organic feel. The starting point for my method was inspired by this post.

Parameters:The cave system generator has several parameters.

  • Seed - integer used to seed the pseudo-random number sequences
  • SystemSize - how many caves deep and wide the system is
  • CaveSize - width and height of each cave
  • MaxWorkers - count at which to deactivate all workers and stop looping
  • SpawnChance - percent chance to spawn new worker
  • Threshold - distance from center at which worker movement becomes harder
  • Layers - how many data arrays to layer into one cave

Create an Array: First I create a two-dimensional integer array to hold the data. For my purposes a 256 x 256 array is large enough. The array is filled with integers that represent the tile type at an (x,y) position. Currently I have a type index for wall, floor, transition up and transition down. I actually use a one-dimensional array and access it like so:

int get(int x, int y) {
    return data[x + width * y];
}

void set(int x, int y, int val) {
    data[x + width * y] = val;
}

Create Workers: Next, I create a worker object, activate it, drop it in the center of the data array and set the value at its location to the floor value. Now I start looping through all active workers. During the loop, each active worker is given a procedurally shuffled set of four directions: N, E, S and W. Each direction is checked in order from first to last; if a wall is encountered in the checked direction, the worker is moved to that location and the value of that location is set to the floor value. If no walls are encountered, the worker is deactivated and removed unless the worker is the last active worker; the last active worker will move, without spawning new workers, until it finds a wall. Each time a worker moves, it has a chance [SpawnChance] to spawn a new worker. Again the parent worker is given a procedurally shuffled set of four directions. Each direction is checked in order; if an unoccupied floor is found in the checked direction, a new worker is spawned in that location and the worker count is incremented. If no unoccupied floor is found, the new worker is not spawned. If the distance from the center to the worker is farther than the [Threshold] parameter, the worker’s movement in the direction away from the center becomes exponentially harder the farther it moves past the threshold. If a worker reaches the edge of the data, it is re-positioned at the center. After the worker count reaches the [MaxWorkers] parameter, all workers are deactivated and the loop exits.

Layer Data: The previous steps are performed [Layers] times and each set of data is layered on top of the last. Wall values are changed to floor values only. At this point, we have something that looks like this:roughSmoothing: The next step is taking the combined layers and smoothing the result. I split my smoothing algorithms into two functions simpleCleanup() and complexCleanup(). The simple function loops through each wall and checks the surrounding values in opposing pairs; if the N and S values are floor or the E and W values are floor, the wall is set to floor. The complex function is a little more, well, complex. It loops through each wall and if at least three of the NW, NE, SW, and SE values are floor, the wall is set to floor. To achieve the smoothed data you see below, first the simple and complex functions were performed each four times, starting with the simple one and alternating between the two; simple -> complex -> simple -> complex -> etc. Finally two simple passes are performed. This results in the smoothed, organic looking cave below. Smoothed cave after cleanup.Resize: After each map has been created and smoothed, the minimum and maximum x and y values are calculated and snapped to a grid based on my game’s x and z chunk size, which is 16 x 16. Then, if the cave’s floor is too close to the edge, that edge is padded with a strip of chunks full of wall values. The map is re-mapped to a new map with the new size.

Transitions: To connect the caves in the system, I first generate a maximum number of transitions for the first cave. This number is proportional to the number of floor tiles in the map. Next, a random number of transitions are generated, not to exceed the maximum number of transitions. A map is then generated and linked for each transition, provided the number of maps at the current depth does not exceed the maximum cave system width parameter. If the map depth equals the maximum cave system depth parameter, no transitions are generated and the cave goes no deeper.

Extra: I perform some extra, game-specific steps. I generate a list of indexed chunks for each map and each chunk object contains data about the 16 x 16 region; data such as how many floor tiles are in the chunk. The image of the caves at the top demonstrates this by coloring each chunk a brighter shade of green the more floor tiles it contains.

Introductory mock-up with 3D view Indexed palette and editing capability Arbitrary sized sprite maps and file IO Converter to load the old LSS sprite format Axis indicators, paint / add mode, slice bounds visualization

Sprite editor in progress… will be in the update this weekend!

I had to write a converter to be able to load in the old LSS sprite format. As you can see by the picture with the orange hair, the old format was somewhat wasteful. Also, I ran into a problem: the old format uses three floats per voxel to represent color whereas the new format uses 16 indexed colors. This means that the old format used way more than 16 colors.

When I first loaded the sprite with orange hair, I used a simple bit of code that just  put the first 16 unique colors into the swatches. All swatches were filled with various shades of orange. Not good.

So I thought about it a little bit. RGB values from 0.0f to 1.0f can be thought of as three dimensional points in the first (+, +, +) octant. Therefore it should be pretty straightforward to use the distance formula [ d = sqrt(deltaX^2 + deltaY^2 + deltaZ^2) ] as a measure of how similar two colors are. Testing against a threshold [ if d < threshold … ] allows subsequently discovered colors to use an existing indexed color if the new color is close enough.

It’s not perfect as it still produces some anomalous results, but it’s good enough to retrieve some of the hard work I put in on the old sprites a year ago.

Current features include:

  • Saving / loading sprites
  • Importing / exporting palettes
  • Importing old LSS sprite format
  • Slice bounds visualization in 3D view
  • View only selected slice in 3D view
  • Axis indicators in 3D view
  • View any of the 4 views fullscreen
  • 16 indexed colors
  • Paint / add / erase in slice view
  • Sprite sizes (1, 1, 1) - (64, 64, 64)
  • Rotate / pan / zoom 3D view
  • Paint / add / erase in 3D view 
  • Slice fill (not pictured)
---------------------------------------------------------------
Language     files          blank        comment           code
---------------------------------------------------------------
Java           380          10137           4127          26833
PHP            309          10275           5858          26722
CSS              6            641             68           3242
GLSL            54            407            111           1579
Javascript      14            262             89           1192
XML              9            111             12            574
HTML             3              8              0             42
DOS Batch        1              0              0              1
---------------------------------------------------------------
SUM:           776          21841          10265          60185
---------------------------------------------------------------

I didn’t realize I had a problem with my aspect ratio until I added a sphere model. Seems I forgot to cast my int width and int height to floats before aspectRatio = width / height; which was giving me a 1 for all resolutions. Man, what a rookie mistake.

I&#8217;ve been working on the launcher, server api and getting the two to communicate. Here is a screenshot of the launcher right after I got it to successfully validate user credentials against the remote database. There&#8217;s some fake, mock-up style update data being loaded from a tumblr blog in the middle.

I’ve been working on the launcher, server api and getting the two to communicate. Here is a screenshot of the launcher right after I got it to successfully validate user credentials against the remote database. There’s some fake, mock-up style update data being loaded from a tumblr blog in the middle.

The SSAO is finally working! It needs tweaking and optimization, but the important thing is it works.

I use an interpolated view ray to reconstruct the view-space position of each pixel from the depth buffer. To get the view ray, I pass in the four corners of the frustum’s far plane and let the hardware interpolate the ray in the fragment shader. It turns out that in calculating the geometric representation of my frustum, I forgot to convert my field of view from degrees to radians. I spent hours tweaking and debugging the shader code. Sorry, I mean days. The screenshot on the right is the type of stuff I’ve been looking at for the last week.

The position buffer you see in the left screenshot is temporary. I used it to test if the reconstructed view-space position was accurate.

Next, I plan to optimize / tweak the SSAO, add a blur pass, and re-organize / pack the G-buffer.

Update on the new Lodestar engine:

  • Octree based frustum culling
  • Dynamic loading / unloading of tile chunks
  • Bounding box and chunk data pools to re-use objects