Energy Grid Update - Nearing The End

After my last update, I have since balanced a lot of the numbers, giving the player a choice in how to collect resources. Pollution in the game exists as a way to punish the player for building unclean energy gatherers. Pollution has been changed to be a tax percentage rather than a flat amount. Pollution as a flat amount could cripple the player at the start, and not matter later. Now it scales much better. This means that as your gatherers collect resources, and those resources are sold, pollution tax is deducted from that income as a percentage.

Another major change is infinite upgrade. The upgrades that increase the resources gatherers get per tick have this feature enabled. As you research the upgrade, the cost goes up by 10% each time. This can really allow you to take the game far.

After making these changes, I noticed an issue. As you would collect resources the power bar would show a number. This number is power demand - power output.

power-bar.png

When it’s in the negative, you’re slowly running out. When it’s in the positive the number is filling up. When you would hit the positive, the game would add another city which increases the power demand, putting it back into the negative. As the player goes through the game and more cities get added, the rate of power consumption would keep increasing. Eventually it would consume faster than I could click sell. So what I decided to do was get rid of having you click the button to sell, and make it part of the gather operation. This means that when resources are gathered, they are sold immediately. To better illustrate this, I updated the side panel UI to show gathering rates and income.

gathering-ui.png

To keep the UI updates in sync, I had to update the games systems to do the calculations of gathering, pollution, selling, and power provided in a single pass, rather than across multiple systems. Without this, even with the automated selling, you’d hit a lose condition from the power demand getting too fast.

Further more, I added a button in for you to add another city to provide power too, rather than it being automatic,. So you can control the difficulty as you progress.

UI improvements around pollution

Another nice little improvement I worked on recently, is showing the player which tiles will be effected by pollution as you build different gatherers. You can see the effected tiles below in red.

pollution-selection.png

What’s next?

The game is getting fairly close to done. I’m working on the menu screen, and am looking to make adjustments to the settings UI. Then work on an end game screen as well.

I also plan to take another pass at the music, as I haven’t updated it since the original Ludum Dare entry.

Energy Grid Map Generation

As i've been balancing the numbers for the game. I realized that the way to win was to tech up as fast as possible, and just put solar panels everywhere. This just didn't have much in terms of strategy. I want the positioning of your gatherers to matter more. The simple way to do that is reduce the number of places you can build them.

The existing map generation is really simple. It finds an open 3x3 grid on the map where it can build a set of tiles, using a random number. The tile types currently in game are: Swamp, City, River, Open. The center of the 3x3 would always be something other than Open, and the surrounding tiles would be dependent on the center. Rivers more likely around a city, swamps more likely around a river. To reduce the available space problem, I tried increasing the number of 3x3 grids, but i could only do up to 4. A number of instances the game would never start as it couldnt find enough space for 5. So I decided to rethink how I generate the map.

The first option was to use the same 3x3 generation, but break the map out into 5 sections so it could reliably do this. Much like so:

sections.png

Then 5 3x3 grids would be able to fit, so long as they confine to the section they're in. However when I started filling out different patterns, I wasn't feeling too keen on it as a solution.

sections-painted.png

This scenario for example leaves some large chunks of space, not really creating confined areas that the user is forced to place in gatherers. The idea of constricting the space is so the user has to consider how much pollution they create. So I had another thought. What if I set the open space using defined shapes? What about the letter L. I keep the width of the L set to two spaces, using a variable length for the long part and the short foot of the letter. I then also sketched out some on how this might look, going with the assumption I would apply four "L"s to the map.

lshapes-painted.png

I realize this looks confusing, as a number of the "L"s overlap there, but the yellow space is blocked, the open space is where you can build. In most cases, you're almost always touching protected space. Meaning pollution is hard to avoid. But there's still enough squares for you to work with to get enough energy.

To pull this off in code, the main piece was tracking horizontal & vertical "L"s to ensure i dont have them overlap 100%. So if a vertical rotated L sits on more of the left side of the map, make sure the next one sits  on the right side somewhere. Otherwise it was just a matter of mapping the X & Y values to these Ls to build out the list of open squares.

Then the yellow space is filled with protected tile types. Some remaining code to sort out is generating those protected tiles more logically. As right now it's a bit of a mess!

ingamemap.png

Spring update on Energy Grid

Back in the fall I started on a roadmap for Energy Grid, to figure out where take it. The main components that I set to focus on was rendering the tech tree. This led me to having to build out a number of systems, which led me to new challenges such as font/text wrapping.

Here’s where I’m at in the road map.

  • [x] Replace original tech upgrade system with 3 nodes in the tech tree, one for coal, oil, and solar power. With coal already being researched. As you sell resources, you receive money. Money should be able to be used to upgrade the other two types.
  • [x] Show tile researching in game menu
  • [x] Create new tile types: Cities, Rivers, Swaps/lakes. Update the map generation to place these in little hubs. Coal, oil, and solar have to be built on blank tiles.
  • [x] Add hydro power, only buildable on river tiles.
  • [x] Add the pollution system. Coal & Oil pollute adjacent tiles with rivers, swaps, or cities in them. Hydro pollutes the river tile it is on. The more tiles polluted, the more amount of money you get taxed on per second.
  • [ ] Implement the rest of the tech tree nodes. The tech tree itself is layed out, but many of the bonuses do not work yet. The remaining bonuses to be implemented can be seen in my last update.
  • [ ] Sell solar panels for money. Enabled via the tech tree, but acts as another source of income, instead of being a passive bonus.
  • [ ] Additional cities to power. This is to ramp up difficulty as you progress
  • [ ] Create final assets for the game.
  • [ ] Create menu screens
  • [ ] Create tutorial/intro to the game

Here’s the game in action as of today:

spring_update.gif

Technical challenges

Drawing the Tech Tree

With the tech tree having over a dozen nodes, with connecting lines, I wanted to accomplish two things:

The first, manage the tech tree with a data file. Managing positions and all the information in code would have been tricky. Given the ECS library I’m using has a bit boilerplate to create a new entity with a set of components, managing this with a dataset instead would be easier to adjust and make changes.

To make this happen, I created an upgrade struct, and decorated it with serde so JSON can be deserialized into it.

#[derive(Serialize, Deserialize)]
pub struct Upgrade {
    pub buff: Buff,
    pub time_to_research: f32,
    #[serde(default)]
    pub current_research_progress: f32,
    pub cost: i32,
    pub status: Status,
}

Then in the JSON file, if a given object has an array of children, my rust code would iterate through that to establish the parent/child relationship. As the research system completes an in progress upgrade, it checks this tree of parent/child to update the status of the child upgrades.

The position of each node in the tree is based on simple number values in the JSON objects. The Y depth I initially tried to do it based on node depth in the JSON tree, but that led to some rows being too packed. So instead I define the Y value as a tier. 1, 2, 3, 4, etc. This is then multiplied to position them in a nicely spaced out way. X is 0->1 value, where 0 is the furthest left in the tech tree container, 1 is the furthest right. Determining the numbers here required a bit more consideration to position the nodes evenly.

The second thing: create an arbitrary shape drawing API. Because I’m using a wrapper of code around OpenGL, I need to draw things with triangles. For a rectangle this is pretty straight forward to put together, but for a polygon with 7 sides, knowing how the triangles should make up the shape becomes complicated. This is known as tessellation. Thankfully Lyon has a tessellation crate to give me this information. I was able to use it to produce the vertices I need to draw an arbitrary shape.

The recursive function that goes through determining the x & y coordinates of each node, this is then used to determine the 4 points of each line, going from node to node.

let last_half_x = last_position.x + SIZE_F / 2.0;
let last_half_y = last_position.y + SIZE_F / 2.0;
let half_x = x + SIZE_F / 2.0;
let half_y = y + SIZE_F / 2.0;
let points = vec![
    Vector2::new(last_half_x, last_half_y),
    Vector2::new(half_x, half_y),
    Vector2::new(half_x + 2.0, half_y),
    Vector2::new(last_half_x + 2.0, last_half_y),
];
let entity = world
    .create_entity()
    .with(Shape::new(points, [0.7, 0.7, 0.7, 1.0]))
    .with(Transform::visible_identity())
    .build();

The Shape struct is my piece of code that leverages lyon to do the tessellation. Building out the vertices is done via:

let mut path_builder = Path::builder();
for (i, point) in points.iter().enumerate() {
    let p = lyon_point(point.x, point.y);
    if i == 0 {
        path_builder.move_to(p);
    } else {
        path_builder.line_to(p);
    }
}

path_builder.close();

let path = path_builder.build();
let mut buffers = VertexBuffers::new();

// Create the tessellator.
let mut tessellator = FillTessellator::new();

// Compute the tessellation.
tessellator
    .tessellate_path(
        path.path_iter(),
        &FillOptions::default(),
        &mut BuffersBuilder::new(&mut buffers, VertexCtor { color }),
    )
    .unwrap();

Then i use the buffers variable inside my renderer, and pass it along with the Color data to my shader.

Map Generation

Procedural generation is not something I’ve done very much of. I knew that just looping through the 10x10 grid and selecting a tile type based on a random number would not be ideal, and would likely lead to not fun scenarios. So I started thinking about how one can make small dense areas on the map of the different types.

It got me thinking of a map with small hills, where ground level would be empty, ground+1 would be swamps, ground+2 rivers, ground+3 would be cities. To keep it simple, why not pick a node with the 8 surrounding nodes free, pick a random value between 2-4, and set the tile based on that number. Then set the 8 remaining tiles 0-(n), n being the value the center tile was. So it cannot be higher than the middle tile, but it can be lower, even empty.

let mut x = 0;
let mut y = 0;
// find the center first
loop {
    x = rng.gen_range(1, 9);
    y = rng.gen_range(1, 9);

    let mut all_nodes_free = true;

    'check_nodes: for i in 0..3 {
        for j in 0..3 {
            if set_nodes.contains_key(&(x + i, y + j)) {
                all_nodes_free = false;
                break 'check_nodes;
            }
        }
    }

    if all_nodes_free {
        break;
    }
}

First, I just do a naive random check to find an open set of 9 nodes.

Then i choose the value of the center node. These random numbers I am likely to change to better balance the map generation.

let weight: u32 = rng.gen_range(0, 101);
let mut highest = 1;
let tile_type = if weight >= 90 {
    highest = 4;
    TileType::City
} else if weight >= 75 {
    highest = 3;
    TileType::River
} else {
    highest = 2;
    TileType::EcoSystem
};

EcoSystem is what I called the swamp internally.

Then it was a matter of looping through the 3x3 grid in this 9 tile space to select the new types. Just using some random numbers for the different values.

for i in 0..3 {
    for j in 0..3 {
        if x + i == center_x && y + j == center_y {
            continue;
        }
        let tile_type = if highest == 4 {
            let weight: u32 = rng.gen_range(0, 101);
            if weight >= 90 {
                TileType::City
            } else if weight >= 75 {
                TileType::River
            } else if weight >= 55 {
                TileType::EcoSystem
            } else {
                TileType::Open
            }
        } else if highest == 3 {
            let weight: u32 = rng.gen_range(0, 101);
            if weight >= 75 {
                TileType::River
            } else if weight >= 50 {
                TileType::EcoSystem
            } else {
                TileType::Open
            }
        } else if highest == 2 {
            let weight: u32 = rng.gen_range(0, 101);
            if weight >= 60 {
                TileType::EcoSystem
            } else {
                TileType::Open
            }
        } else {
            TileType::Open
        };

        set_nodes.insert((x + i, y + j), (tile_type, None));
    }
}

We skip center, as that is already set. Then a different set of random numbers are used depending on what the center number was. The reason it’s done in a long set of if statements is so that it is easier to adjust. I could use an array of numbers to make this cleaner in code, but I didn’t want to couple the generation with the implementation when it’s still relatively easy to follow.

What's Next?

The next major focus is implementing the tech tree passives, and then testing them out. See how things work out balance wise, and how the game feels around the changes. After that, I will work towards making the city tiles what you are powering, and reserve tiles on screen for other cities to require power.

Taking My Jam Entry Further

My last post on here was about an archery game I started. That’s still a game I want to build, but at the end of July I decided to participate in Ludum Dare. Knowing I could re-purpose components and systems I built for the archery game, I felt confident that I could do a game jam with Rust.

You can play the game I created here: https://ldjam.com/events/ludum-dare/39/energy-grid

Somethings went smoothly, others not so much. I had to figure out how to include things like fonts and sound in an async context when neither of those are thread safe. The short answer is to not include them in an async piece of code at all! The Rust compiler is pretty good at making sure you avoid race conditions, so really it’s best to listen to its errors, and re-think your design. In the end I had moved the actual font texture creation & playing the music into the main loop, which runs synchronously. The game systems written on top of Specs - Parallel ECS would simply flag data as being ready to draw new text, or play new sounds.

After completing this, and making some improvements to the draw code, I ported those back into the archery game. While my overall score in the game jam wasn’t that great, I had some really encouraging comments on my entry. So I’ve since then decided to expand it.

I’ve spent the last couple of months implementing their feedback, as well as my own changes:

  1. Allowing one to make any of the researched gatherers.
  2. Changing the selling energy concept to give you money, which you use to create more gatherers, or advance in tech.
  3. Implemented a proper scene graph.
  4. Gave the game a proper restart, instead of just exiting the process.

Now that I have the game in a good spot, I figured it’s time to start working on a proper game design. While I’ve always valued planning when it comes to projects at work, it’s not something I’ve done in depth for a side project. My game Snowball Effect was done at a high level when I worked on V2, but I didn’t have it all flushed out from the get go, I did it in a very agile way. So I’ve decided to really dive deep on this game, and not just figure out what the features are at a high level, but really think it through, even the numbers.

The map instead of just one tile type will have multiple. Ones that you can’t build on, can’t pollute, ones that you can only build certain types next to, etc. This will be used to create a new random map whenever you start a new game. Meaning saved games will also be a thing.

There will be a tech tree of how to go to other technologies, and how to research passive bonuses. The numbers in the below tree are place holder, I’ve worked out more accurate numbers that roll over better (to avoid remainders). I’m sure beyond that it will still require balancing.

EnergyGridTechTree.png

I’m in process on figuring out a road map, and the UI design for the tech tree, along with new features. I look forward to sharing the gameplay of these changes once it’s ready.

Tilemap Parsing, Ground Detection

The past few months have been a fair bit of learning on doing graphics programming, and how to use some of the existing libraries with Rust. Now that I have some foundation, I've been able to start on actual logic needed for a game.

The first steps of this game has been getting tile map rendering working, and interacting with that tile data. I'm a fairly big fan of using Tiled. It's a fairly versatile editor, and a number of engines have direct support for it. In this case I'm working with the data more directly, as there's no real core support for tiled in popular Rust libraries. I'm using an awesome crate called tiled to parse the data into types, so I have that covered. It gives me layers in an array, each layer containing the tile IDs, right from the XML data. Tiled exports a number of formats, and I'm using CSV format. For example:

<layer name="back" width="30" height="20">
  <data encoding="csv">
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5
</data>
 </layer>

The layer is placing a number of tiles in various coordinates. It's one long list, but because the tilemap stores its number of tiles by width and height, it can keep track of what number is in what row & column fairly easily. From there, it's a matter of grabbing the 5th and 6th tile from my tileset. My tileset is an image of 92x64, with the tiles sized at 32x32, so that means 5 & 6 are on the 2nd row. Writing some simple code with division & modulous operators, one can map the UVs pretty easily:

let iw = image.width as u32;
let ih = image.height as u32;
// how many tiles is it wide & high? In this case it's 3x2
let tiles_wide = iw / (tileset.tile_width + tileset.spacing);
let tiles_high = ih / (tileset.tile_height + tileset.spacing);
// how much from 0-1 does the 32x32 take up of the source image
let tile_width_uv = tileset.tile_width as f32 / iw as f32;
let tile_height_uv = tileset.tile_height as f32 / ih as f32;
// cell is the number 5 or 6 from the example above
// subtract 1 to make it zero indexing. Then it's the same math for x & y. Just use width vs height, and modulous vs division.
let x = ((*cell as u32 - 1u32) % tiles_wide) as f32 + tileset.margin as f32 / iw as f32;
let y = ((*cell as u32 - 1u32) / tiles_wide) as f32 + tileset.margin as f32 / ih as f32;
let i = index as usize;
let tiles_wide = tiles_wide as f32;
let tiles_high = tiles_high as f32;
// now we just map the quad against those coords, i being the current index for the quad
vertex_data[i].uv[0] = x / tiles_wide;
vertex_data[i].uv[1] = y / tiles_high + tile_height_uv;
vertex_data[i + 1].uv[0] = x / tiles_wide + tile_width_uv;
vertex_data[i + 1].uv[1] = y / tiles_high + tile_height_uv;
vertex_data[i + 2].uv[0] = x / tiles_wide + tile_width_uv;
vertex_data[i + 2].uv[1] = y / tiles_high;
vertex_data[i + 3].uv[0] = x / tiles_wide;
vertex_data[i + 3].uv[1] = y / tiles_high;

That's the background layer, the next layer is the ground. This goes through the same code to figure out the drawing, but i've added some additional logic so we can build out a set of data for movement.

 <layer name="ground" width="30" height="20">
  <data encoding="csv">
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2,0,0,0,0,0,
2,2,2,2,2,2,2,2,2,2,0,0,0,0,0,0,0,2,2,2,2,2,1,1,1,2,2,2,2,2,
1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1
</data>
 </layer>

0 value tiles are simply empty. They are full on ignored. So I just have a few tiles for a platform higher up, and then a base ground. In this game, i'm looking at making the movement point to click, so therefore use pathfinding, much like how you might make an AI go from point A to B. Initially I had a meta layer, where I placed a tile for each spot the player could move to, but that means for each new type, I'd have to add additional tiles, such as ledges, higher up areas that you should jump to, etc. So instead, why not create this data programatically?

When parsing a layer, the code is going in a top-down direction, so we iterate each row, then each cell across the columns for that row. This means that when parsing the ground tiles, we can track the open tiles above them to build a set of walkable areas.

The first step is walking through and building out this data.

let mut tile_map_render_data: Vec<PlaneRenderer<R>> = Vec::new();
// For the ground tiles, they will be stored x by y, in order to be able to group them. This is explained further down in this blog post.
// You'll note usage of LinkedHashMap. This is from a crate, not the standard library. It gives us an ordered map, so we can preserve the order we parse the ground tiles, and group them.
let mut ground_tiles: LinkedHashMap<i32, Vec<i32>> = LinkedHashMap::new();
// Storing the data y by x, as that's the order the layer is parsed in from a hierarchy perspective. Rows = y, cols = x
let mut unpassable_tiles: HashMap<usize, Vec<usize>> = HashMap::new();
for layer in map.layers.iter() {
    // I mentioned the meta layer earlier. Not in use right now, but keeping this around as it might come in handy still
    if layer.name != "meta" {
        // just building the render data
        let tilemap_plane = TileMapPlane::new(&map, &layer);
        tile_map_render_data.push(PlaneRenderer::new(factory, &tilemap_plane, tiles_texture, target));
        // collision layers being a static array that contains "ground"
        if COLLISION_LAYERS.contains(&layer.name.as_ref()) {
            // a simple function i created for iterating through a layer.
            for_each_cell(&layer, false, |x, y| {
                // building out the map of impassable tiles
                if unpassable_tiles.contains_key(&y) {
                    let mut xs = unpassable_tiles.get_mut(&y).unwrap();
                    xs.push(x);
                } else {
                    unpassable_tiles.insert(y, vec![x]);
                }
                // if not the first row, as nothing will be above it!
                if y > 0 {
                    // if above row above has collision data, hence the y - 1
                    if let Some(xs) = unpassable_tiles.get(&(y - 1)) {
                        // if it does not contain one for this column
                        if !xs.contains(&x) {
                            add_column_above_to_ground(x, y, &mut ground_tiles);
                        }
                    // or if it has zero collision data
                    } else {
                        add_column_above_to_ground(x, y, &mut ground_tiles);
                    }
                }
            });
        }
    }
}

fn add_column_above_to_ground(x: usize, y: usize, ground_tiles: &mut LinkedHashMap<i32, Vec<i32>>) {
    // it is open, so let's add it
    // we track x by y instead of y by x, as we need to go in that order for the tile grouping of grounds
    let x = x as i32;
    let y = (y - 1) as i32;
    if ground_tiles.contains_key(&x) {
        let mut ys = ground_tiles.get_mut(&x).unwrap();
        ys.push(y);
    } else {
        ground_tiles.insert(x, vec![y]);
    }
}

With that data together, we can break it down into groups. The reason for building out the ground_tiles as X by Y instead of Y by X, is to parse each column one at a time. This ensures that when checking a single tile, we can check the full range of the previous column to know what group it falls into. If you go Y by X, you have the current row's data as you go left to right across it, but you won't know if the row below is accessible by the current tiles.

// still storing Y by X, to keep it consistent
let mut groups: Vec<Vec<(i32, i32)>> = Vec::new();

for (col, rows) in ground_tiles.iter() {
    for row in rows {
        let mut found = false;
        let mut temp_row = 0;
        let mut temp_col = 0;
        let mut target_group_index = 0;

        // find whichgroup of (y, x)s can be used
        for (i, group) in groups.iter().enumerate() {
            let last_cell = &group[group.len() - 1];
            // if the X (col) is 0 or +1 to the right. If the Y (row) is between +1 and -1 from the last
            if (col - last_cell.1 == 0i32 || col - last_cell.1 == 1i32) && row - last_cell.0 < 2i32 && row - last_cell.0 > -2i32 {
                temp_row = *row;
                temp_col = *col;
                target_group_index = i;
                found = true;
                break
            }
        }

        // when its found, it means it can be added to a previous group, as it is within walkable range
        if found {
            groups.get_mut(target_group_index).unwrap().push((temp_row, temp_col));
        } else {
            groups.push(vec![(*row, *col)]);
        }
    }
}

With that data sorted out, we can then turn it into our usual Y by X map. Just faster to retrieve the data we need.

let mut hash_groups: Vec<HashMap<usize, Vec<usize>>> = Vec::new();

for group in groups {
    let mut coords: HashMap<usize, Vec<usize>> = HashMap::new();
    for (y, x) in group {
        let y = y as usize;
        let x = x as usize;
        if coords.contains_key(&y) {
            let mut xs = coords.get_mut(&y).unwrap();
            xs.push(x);
        } else {
            coords.insert(y, vec![x]);
        }
    }

    hash_groups.push(coords);
}

Snowball Effect Skins

As the game gets closer to release, I've worked on a few skins that you'll be able to purchase, and use to your heart's content. I wanted to make some that were a bit out there, and out of place, to make things more fun.

Since animating a rotating sphere proved difficult, I decided to make a flat texture, and leverage blender to make some of the frame by frame animations for these. First time doing 3d texturing in well over a decade, UVs are hard! I give much respect to those who work on 3d games, and animated motion pictures.

Here's the first skin I completed:

It's a large rock that gains & shrinks just as the normal snowball. I experimented with more of a golf ball type look, but felt it didn't give the look I want. So I ended up using a low poly sphere.

The next skin is a bowling ball.

The type of ball that most of us have rolled along a surface at some point. Bowling balls come in a range of patterns and designs. I wanted a nice blue one to keep with the existing colour theme. Then lined it with red, purple, green, orange bits so you can see it spin. Along with the typical three hols to grip the ball with.

To follow the trend of games that you play for a fun night out:

I naturally had to add an 8 ball. Making the texture for this was pretty straight forward, but as I mentioned earlier, getting the UVs right so it wasn't stretched was a challenge. I'm fairly happy with the result now, and I hope you like it.

By far my favourite skin though is this one:

I generally haven't caught on to the retro + pixel art trend going on in games, but I understand the appeal. I decided to give this a try, and I'm really happy how it turned out. I initially tried the same base colour as the standard snowball, but I found white worked better. I also added square versions of the little spots that wrap around the snowball.

Game Progress

As far as the state of development, I'm on the final touches of the game now. I'm working on getting game music complete, as well as working on an android build. Look out for a release coming soon!

Revive item implemented - Snowball Effect

This latest update includes the system for purchasing items, and the implementation of the revive item. So after doing a few runs and collecting coins, you can then buy yourself one or more revive items. If you have any avaialble, you'll see an additional button upon death

Using this will then bump up your snowball size immediately, and allow you to continue the game where you left off.

Snowball Effect, Challenges

As mentioned in the previous post, I altered the speed a little. Slowed it down to allow more reaction time, and give the player a better chance to see things as they happen. Feeling fairly happy with this change. Another change I needed to make concerned the UI, as I added in some new mechanics and the UI needed to be shifted around. You can see I moved the resource bar moved to the top.

The reason for this was the addition of a jump ability, the next addition to the player's toolbox. Upon completing the first challenge, the player will unlock the jump ability. The game will then start spawning a trap that requires a jump to get over it. You can see the jump button on the bottom right.

The jump also can be used to avoid the fire pits, but it does cost a bit more energy to use over throwing the snowball. Now, one can use it to go over multiple fire pits, depending on how they stack. So having both the jump & throw abilities can help for different situations.

In order to unlock the jump you need to complete a challenge. The actually criteria for the challenge hasn't been nailed down, but for now it's to throw 5 snowballs at 5 pits. When the game starts up, it will show you want your current challenge is.

Next Steps

Aside from a couple bugs I wish to tackle, the next set of things to work on will be primarily adding further challenges. Then after that start looking at powerups/items to use as you go through the game. This will involve both the implementation when going through the game, as well as a UI screen for it. I am considering doing a UI screen for the challenges you have yet to unlock as well.

Snowball Effect, new mechanic

One of my focuses with doing the new version of this game was to add abilities and other mechanics for the player to use. The first one I have now implemented is simply firing a little snowball on tap at a fire pit. This doesn't remove the fire pit from game, but changes its state so it no longer does damage to the player's snowball.

I played with an alternate input mechanism for this mechanic. I thought it would be neat to have the player swipe from either side of the screen to fire a snowball from that side of the screen at the closest target. This presented however a couple of problems:

  1. With gyro/tilt controls off, touch input was needed to move the snowball. How to best differentiate between swipes and movement intent?
  2. The closest target may not be the one the user wishes to avoid.

To further the last point, I tried playing with the code a fair bit to see if I could make it intelligent, but it never felt quite right. So I decided to go with the implementation that I settled on. Which is to simply tap the target. Upon doing so, a snowball flies across the screen. Upon collision with the fire pit, it changes to a disabled state:

Of course to prevent the user from staying still and constantly tapping at every hazard on screen, they need to gather a resource in order to fire a snowball. This is done by colliding with the blue shapes on the hill. As they do the bar on the right side fills up:

Next steps

I'm considering changing the pacing of the game a little bit with this change. It's hard to make out what the snowball actually is, as it flies across the screen rather quickly. Slowing the game down could make it more identifiable. Make fewer hazards and resource targets appear, but make them larger as well. Leading to more strategy on positioning instead of reaction. Not sure if this is the way I will take the game, just something I wish to play with.