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.

Snowball update, random infinite runner

The game has not changed much looks wise since I last posted, but I've gotten some important functional changes done. The game now works with a randomly generated route, instead of a fixed one. What this means is the game can be truly infinite, and dynamic as I add more permutations and type of terrain.

To get into the technical changes for a bit, the original code uses a z position, which goes up in value each frame to move through the path. Now though once you pass a segment of the ground, it gets removed from the scene, and put back into the object pool. Once a full section is passed, such as a: hill, straight away, curve, or s curve, a new random section is added. This will go on indefinitely.

Originally each segment stored its index, in terms of where it is in relation to each other segment. I dropped this, in favour of using its array index instead. This allowed me to pull the first element of the array off easily, and add new ones to the end, without having to update a property. However, I did have to update the "world position" of each segment when a segment is removed. The world z position is based off of its array index, so when the array index changes, so must the world position. This is an extra O(n) operation I would really prefer to avoid, so it's something I'm going to have to continue to think on.

cleanupSegments() {
  const baseSegmentIndex = this.findSegmentIndex(position - SlopeBuilder.SEGMENT_LENGTH);
  const segment = this.children[baseSegmentIndex - 1];
  if (segment) {
    me.pool.push(segment);
    this.children.splice(baseSegmentIndex - 1, 1);
    this.setTrackLength();
    position -= SlopeBuilder.SEGMENT_LENGTH;

    // TODO: Think if there's a way to avoid looping through all segments
    for (let i = 0; i < this.children.length; i++) {
      this.children[i].resetWorldZ(i, SlopeBuilder.SEGMENT_LENGTH);
    }
  }
}

As you can see here, I find the segment behind the current camera position. Fetch the segment from that. If a segment is returned, it gets pushed to the pool, removed from the collection. The setTrackLength method takes the children.length by the SEGMENT_LENGTH to determine the current length of the track, and cache it accordingly. The camera position is then negated by the SEGMENT_LENGTH as well. While one solution is to increment the position forever, numbers do have maximum storage values. So I went for the route of subtracting the position, and update the world position on each segment in the game world.

While the route generation is mostly random, down hill has a higher weight, so it will spawn a majority of the time. This is to give the feeling of still going down a large slope. I re-implemented the fire pit hazard, along greatly increased the rate at which collidable objects spawn on the route. Something I'm going to do soon is replace the art assets of both objects. I'm planning to simplify them on detail and number of colours, make it look more stream lined with the rest of the game, and go back to vector art for one of the items. The downside right now is the firepits or objects you collide into spawn in 1 of 3 positions in the lane. It looks a little too static, or programmed in. I hope to play with some variance on this, make it look more organic. The frequency of the objects is really high too, so I might try to put in some more non man made hazards, such as rocks and trees.

Thanks for reading. This game has my focus outside of my fulltime job now, so I should be able to post fairly regular updates.

Snowball, rolling hills

I took a break from development on Snowball Effect for around a month. I participated in LibGDX Jam. You can check out my entry here if you wish.

Before the holidays I implemented hills into the generation system. This evening I finally got the sprites clipping properly, which you can see here!

This was accomplished by referencing a few tutorials on building an oldschool racer from the NES & SNES days: codeincomplete. The terrain is in segments. A segment has two pieces of data, each pertaining to the top & bottom sides of the segment. Each side contains an object storing the position, width and scale. It also stores a Vector3 for its original world starting position. This is never changed. The world Vector3 stores the z coordinate, an index of where it sits relative to other segments. It also stores a y coodinate, which is its z coordinate * height.

It then has a Vector3 called camera, which is used to cache the calculated starting values stored in world, and add them against the game's camera position. This is what makes them "move" along the screen.

These vectors are used to calculate the screen position & scale needed each frame. It uses set values I have making up the game world & height.

Segments are drawn from the bottom of the screen, up to the middle where they cap out. As it goes along in a given frame, it stores the lowest Y value achieved. This Y value is stored on each segment. This Y value is then used when the sprites are drawn, from top to bottom, so they draw over top of each other properly.

Drawing a sprite takes its segements top Y value, subtracting its scaled height. It then calculates the amount to clip off by doing:

let clipH = clipY ? Math.max(0, ypos + scaledHeight - clipY) : 0;

So if there's a clipY coordinate, it finds the bottom of the sprite, subtracts the coordinate. Then cap the clipH so it doesn't surpass the sprite's scaled height.

The source image coordinates are relative to the sprite sheet, but the height is then dependent on the clip value.

spriteHeight - (spriteHeight * clipH / scaledHeight)

Height in this case is the sprite's true height, not the scaled value. It subtracts itself, from the percentange that the clip amount takes off of the scaled height. This ensures the amount drawn from the image respects the projection scaling.

The destination height is then simply:

scaledHeight - clipH.

Initially I was drawing from the center of the sprite, so positioned in the middle instead of top left. This lead to a lot of calculation problems with the scaling.

Curves in snowball

It's been a busy week, but I found some time to get back to working on the game. I implemented the much needed curves to the terrain generation.

It feels pretty cool! I definitely need to get it working in random, so I can add a curve, chop off segments behind the player, and then generate new ones on the fly. After that, I will look at adding up & down slopes.

In addition to gamedev, i've been doing a fair bit of advent of code, which is a programmer challenge advent calendar. The problems do tend to align with mathematic challenges, but are more programmer-y than project euler. I've been using Rust to do them, but you can use any language. Check it out if you're looking for a nice break from the usual.