047 - December 22, 2019

Over the last week I started putting in some of the groundwork for adding scenery to Capuchin City, the first city in the game.

Right now the scenery is defined in a .yml file that currently looks like this:

####################################################################################################
#
# Scenery
#
# This file defines all of the scenery in the game.
#
# Right now we keep it all defined as one batch of scenery, but in the future we migth want to split
# this into many batches so that the client can only download scenery information that they need.
#
# These batches might be created by hand - or by an automated process that looks at the positions
# of the different scenery to determine how to group them. We'll worry about this when we have much
# more scenery to manage.
#
# TODO: We eventually want to automatically manage / edit our Scenery from an in-game editor dev tool
# instead of by hand - but that can come when there is much more scenery to manage.
#
# TODO: Use this file to automatically generate the SceneryId enum
#
# @see client-server-common/src/resources/scenery.rs
####################################################################################################

# SceneryId
PookieHouse:
# SceneryItem
    meshes: [SnailBody] # FIXME: Instance rendered Pookie's house fence
    permissions:
      EntireFootprint: AllowAllMovement # FIXME: We want to prevent movement at outer ring, but allow through door
    location:
      Absolute:
          bottom_left:
            x: 10
            y: 10
          tiles_wide: 8
          tiles_high: 8

# SceneryId
PookieDesk:
  # SceneryItem
  meshes: [Desk]
  permissions:
    EntireFootprint: PreventAllMovement
  location:
    Relative:
      rel_bottom_left_of: PookieHouse
      x_offset: 0
      y_offset: 0
      tiles_wide: 2
      tiles_high: 1

One of the more interesting bits of the scenery is that for each piece of scenery we define the permissions that it stamps onto the tiles that it covers.

/// The walkability / flyability of an individual tile
#[derive(Debug, Deserialize, Serialize, Copy, Clone, Eq, PartialEq)]
pub enum IndividualTilePermission {
    /// Allow all movement
    AllowAllMovement = 0,
    /// Allow flying but prevent walking
    AllowFlyPreventWalk = 1,
    /// Prevent all movement
    PreventAllMovement = 2,
}

So if a piece of scenery takes up a 3x3 set of tiles in the grid - we automatically set the walkability/flyability of those tiles based on the scenery in that space.

This is done by deserializing the scenery.yml file into a HashMap<SceneryId, SceneryItem> - resolving any relative positions (some scenery are positioned relative to other scenery) - then stamping permissions onto the TileMap based on where the scenery is absolutely positioned.

All of the above is working smoothly - right now I'm in the middle of updating the pathfinding algorithm to take into account the TileMap's tile permissions as well as updating the pathfinding algorithm to work with arbitrarily sized boxes of tiles instead of only pathfinding between two tiles.

So - for example - we'll be able to pathfind between a 1x1 entity and within some DistanceRange::new(u8, u8 ) of a 3x3 destination.

Making the first Scenery

I modeled Pookie's desk during the week. I planned to model his desk, some of the science equipment on his desk and the fencing (instance rendered) around his house, but I got sidetracked by the setting up the scenery, TileMap and pathfinding data structures.

So I should get back to making these meshes in a few days.

Pookie Desk Blender Shader Nodes The shader nodes in Blender for Pookie's desk.

The desk has 650 vertices - but I'm not really sure whether or not that's too high, too low or just right.

I still need to think through what sort of vertex budget I want.

As someone who is learning to do art - I think that my skill development will be aided by having well defined parameters such as my maximum number of vertices to work with.

At some point I'll want to benchmark the scene on a mobile device and start to get a sense of how much I can render under different performance constraints.

Pookie Desk Blender Shaded Pookie's desk - modeled in Blender. There are 650 vertices, we're using a normal map from a mesh with 12k vertices

Pathfinding algorithm changes

The pathfinding algorithm used to work with only individual tiles. This is a problem because some entities will be able to take up multiple tiles - and we'll need to be able to pathfind between them.

For example - a 1x1 player might be attacking a 3x3 monster - and we need to pathfind the player to be within 0 or 1 tile (or more if you have a long distance weapon) of the 3x3 monster's perimeter.

For problems like this that have lots of different cases to consider - I'll typically start by sketching out test cases.

When the problem is visual - such as this pathfinding one - I use a program called Monodraw to create diagrams to include in the code comments so that if I revisit some code or a test in a few years I don't forget what it's trying to prove or explain.

/// The path should not contain any un-walkable tiles
///
/// DistanceRange::new(1, 1 ) 
///
///
/// ```text
///   ┌───────┬───────┬───────┬───────┬───────┐
///   │ ┌─────┴─────┐ │  ┌────┴────┐  │       │
/// 4 │ │           │ │  │         │  │       │
///   │ │           │ │  │  Start  │  │       │
///   ├─┤    End    ├─┼──┤(Finish) ├──┼───────┤
///   │ │           │ │  │         │  │       │
/// 3 │ │           │ │  └▲───┬────┘  │       │
///   │ └─────┬─────┘ │   │   │       │       │
///   ├───────┼───────┼───┼───┼───────┼───────┤
///   │       │███████│   │   │       │       │
/// 2 │       │███████│   │   │       │       │
///   │       │███████│   │   │       │       │
///   ├───────┼───────┼───┼───┼───────┼───────┤
///   │       │       │   │   │       │       │
/// 1 │  ┌────┴────┐  │   │   │       │       │
///   │  │         │  │   │   │       │       │
///   ├──┤  Start  ├──┼───┼───┼───────┼───────┤
///   │  │         │  │   │   │       │       │
/// 0 │  ├─────────┼──┼───▶   │       │       │
///   │  └────┬────┘  │       │       │       │
///   └───────┴───────┴───────┴───────┴───────┘
///       0       1       2       3       4    
/// ```
#[test]
fn does_not_use_non_walkable_tiles() {
    unimplemented!("")
}

After writing out some unimplemented test cases I'll usually create a test struct that can be re-used across all of these test cases.

struct TileBoxPathfindTest {
    tile_map: TileMap,
    start: TileBox,
    destination: TileBox,
    start_in_range_when: StartInRangeWhen,
    // Starting at the bottom_left tile in the TileBox
    expected_path: Vec<TilePos>,
}

impl TileBoxPathfindTest {
    fn test(self) {
        let mut calculated_path = vec![];
        let mut neighbors_holder = vec![];

        find_path_between_tile_boxes(
            &self.tile_map,
            &self.start,
            &self.destination,
            &self.start_in_range_when,
            &mut calculated_path,
            &mut neighbors_holder,
        );

        assert_eq!(calculated_path, self.expected_path);
    }
}

And then in every test I'll call it with the data that I need for that test like so:

/// Test moving up vertically to get into range
///
/// ```text
///   ┌───────┬───────┬───────┬───────┐
///   │       │ ┌─────┴─────┐ │       │
/// 4 │       │ │           │ │       │
///   │       │ │           │ │       │
///   ├───────┼─┤    End    ├─┼───────┤
///   │       │ │           │ │       │
/// 3 │       │ │           │ │       │
///   │       │ └─────┬─────┘ │       │
///   ├───────┼───────┼───────┼───────┤
///   │       │       │       │       │
/// 2 │       │▲      │       │       │
///   │       ││      │       │       │
///   ├───────┼│──────┼───────┼───────┤
///   │       ││┌─────┴─────┐ │       │
/// 1 │       │││           │ │       │
///   │       │││           │ │       │
///   ├───────┼│┤   Start   ├─┼───────┤
///   │       │││           │ │       │
/// 0 │       │││           │ │       │
///   │       │ └─────┬─────┘ │       │
///   └───────┴───────┴───────┴───────┘
///       0       1       2       3
/// ```
#[test]
fn straight_up_vertical_path() {
    TileBoxPathfindTest {
        tile_map: TileMap::default(),
        start: TileBox::new((1, 0), 2, 2),
        destination: TileBox::new((1, 3), 2, 2),
        start_in_range_when: StartInRangeWhen::StartPerimeterOutsideOfEndPerimeter(
            DistanceRange::new(0, 0 ) ,
        ),
        expected_path: vec![TilePos::new(1, 0), TilePos::new(1, 1), TilePos::new(1, 2)],
    }
    .test();
}

I keep every test to one specific case. This makes it easier to come back to a test suite 12 months later and visualize or fix one specific aspect of the suite vs. needing to wade through a bunch of noise. It also helps me focus on getting one test passing at a time and then moving onto the next one.

It also makes authoring the tests easier because I only need to think about one case at a time - write it down - then move on to the next.

This Week

This week is Christmas! I'll be heading home to spend some time with family - but I'll still have some time to make progress on the game.

I'm looking to finish the new pathfinding between arbritrarily sized start and end TileBoxes, and then get back to modeling more scenery for Pookie's house in Capuchin City.


Cya next time!

- CFN