So this week has been unusually busy for me outside of building the 7drl. I skipped working on the game Monday and Wednesday and only had a chance to work on the game Tuesday and today.
So what I’ve been working on almost exclusively since the last update is the monster behavior. I’ve never actually worked so closely with pathfinding before. Previously I used libtcod to do most of that. For this game I’m using a-star pathfinding from a crate called pathfinding.
Yep, its just called pathfinding. Version “1.1.12” to be specific.
Here is the code that takes care of the Punk movements.
use pathfinding::prelude::{absdiff, astar};
//block map is built from the tiles and entities to see which spaces
//the monster can move and which are blocked
let block_map = self.pathing_map();
//This moster always moves toward the player's position
let goal: (i32, i32) = (player.pos.x as i32, player.pos.y as i32);
//This is the a-star function from the pathfinding crate
let result = astar(
//start: position of the monster
&(
self.entities[it].pos.x as i32,
self.entities[it].pos.y as i32,
),
//successors: this is a closure which returns a map of possible
//next movements paired with the cost it would take to move to each
//for this game, every movement will cost the same time
//currently punks only move orthogonally (up,down,left,right)
//however that created really boring gameplay so I'll have to change it
|&(x, y)| {
let mut succs = Vec::new();
if !block_map[(x + 1) as usize][y as usize] ||
(x+1,y) == goal {
succs.push((x + 1, y));
}
if !block_map[(x - 1) as usize][y as usize] ||
(x-1,y) == goal {
succs.push((x - 1, y));
}
if !block_map[x as usize][(y + 1) as usize] ||
(x,y+1) == goal {
succs.push((x, y + 1));
}
if !block_map[x as usize][(y - 1) as usize] ||
(x,y-1) == goal {
succs.push((x, y - 1));
}
succs.into_iter().map(|p| (p, 1))
},
//Hueristic: an approximate distance to the goal.
//in this case it is the "taxicab" distance however if I introduce
//diagonal movement it will have to be something like
//|&(x, y)| sqrt((x, goal.0)^2 + (y, goal.1)^2),
|&(x, y)| absdiff(x, goal.0) + absdiff(y, goal.1),
// Success: this just checks if you got to the destination or not
|&p| p == goal,
);
//the astar fn returns Option<(Vec<N>, C)>
//this means if it finds a path it returns Some(Vec<N>,C)
//if not it returns None
match result {
//A path was found, lets do something about it!
Some(res) => {
//this gets the destination from the result
let dest = Vector::new(res.0[1].0 as f32,res.0[1].1 as f32);
//I need the relative position to the entity because of the way I
//setup my Move and KillRel Actions
let rel = dest - self.entities[it].pos;
//If the distance to the player is only 1 we kill the player
if res.1 == 1 {
let act = handle_action(self, self.entities[it].id,
Action::KillRel(rel.x,rel.y));
//I dont have any Game Over state yet so this is temporary.
println!("Game Over!");
}
//Otherwise we move one step closer to the player
else {
handle_action(self, self.entities[it].id,
Action::Move(rel.x,rel.y));
}
}
//If there is no path forward do nothing
None => {println!("None");}
}
For a while I was thinking it might be interesting to provide a function that I can plug in for the successors. Then for each type of movement I would just call that function for the entities movement type. That function might also be useful because I want to setup the draw function eventually to highlight dangerous spaces to move to.
However, what I’ve realized is this idea of interchangeable successors doesn’t work for the second entity type I want to implement which is the motorcyclist. This entity is going to move three blocks at a time, and only be able to move to the left. Its going to swerve to try to hit the player but if it misses it will just keep moving left until it dies in the firewall. The issue is that the dangerous spaces would be all three of the spaces the motorcyclist moves through, however only the destination space would be the successor.