2020-10-29

Egregoria Devblog #6

If you want to know what Egregoria is about, here's the first post.

Island generation

Since the black background with a grid started to feel a bit too raw, I used fractal perlin noise to generate an island background. Basically, the process is:

fn height(pos) = perlin(pos) - distance(pos, 0)
water = height < 0.1
sand = 0.1 < height < 0.12
grass = 0.12 < height < 1

Then I'd offset the pos by a random amount to generate a new island and shade the grass and water depending on the height.

Example results:

Final island (subject to change):

I then sprinkled some trees on top of the island, following the terrain slope to make an interesting pattern:

Also added slight moving clouds on top to give a bit more depth:

Specs -> Legion 0.3

After the recent announce of Legion 0.3 I got very excited about the system macro which seemed to fulfill all my needs.
The performance boost from archetypal ECS also seemed interesting.

Porting from specs to legion made quite a big commit:

But as you can see, it did remove a lot of boilerplate.

Here's an extreme example:

Specs
pub struct KinematicsApply;

#[derive(SystemData)]
pub struct KinematicsApplyData<'a> {
    time: Read<'a, TimeInfo>,
    transforms: WriteStorage<'a, Transform>,
    kinematics: WriteStorage<'a, Kinematics>,
}

impl<'a> System<'a> for KinematicsApply {
    type SystemData = KinematicsApplyData<'a>;

    fn run(&mut self, mut data: Self::SystemData) {
        let delta = data.time.delta;

        (&mut data.transforms, &mut data.kinematics)
            .par_join()
            .for_each(|(transform, kin)| {
                kin.velocity += kin.acceleration * delta;
                transform.translate(kin.velocity * delta);
                kin.acceleration = Vec2::ZERO;
            });
    }
}
Legion
#[system(par_for_each)]
pub fn kinematics_apply(
    #[resource] time: &TimeInfo,
    transform: &mut Transform,
    kin: &mut Kinematics,
) {
    kin.velocity += kin.acceleration * time.delta;
    transform.translate(kin.velocity * time.delta);
    kin.acceleration = Vec2::ZERO;
}

However, performance wise, it was very disappointing.
Before porting from specs to legion, I set up a benchmark test which spawned 10'000 cars and 10'000 pedestrians in a grid map and timed the update.

Both legion and specs took the same time, that is about 7-8ms on a single core.
That is, ECS architecture was never the bottleneck, ergonomy is what actually matters, at least for my project.

Therefore, I might consider ergonomic-oriented ECS such as bevy or shipyard in the future.

I also had to reimplement the scheduler since for some reason it had a huge overhead (2-3 ms).
Bonus: I can automatically time each system easily.

The serialization works great to serialize the world but I couldn't find any way to serialize entities contained in resources.
As a workaround, I clone resources containing entities in the world, then serialize the world.
When deserializing, I extract thoses resources and remove them from the world.

I tried looking inside legion's code around serialization but it is very arcane.
It looks like it's using a global state to store the entity serializer, however this global state is not exported so I cannot use it to serialize resources. :(

from legion/src/internals/serialize/id.rs:

thread_local! {
    static SERIALIZER: RefCell<Option<&'static dyn EntitySerializer>> = RefCell::new(None);
}

impl Serialize for Entity {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        SERIALIZER.with(|cell| {
            let mut entity_serializer = cell.borrow_mut();
            // ... serialize
        })
    }
}

If anyone knows how to achieve this, hit me up on Github or Discord.

Very basic human AI

After thinking a bit about how I'll structure my AI, someone on discord mentioned Utility systems.
IT looked very inspiring, as it was very controllable but can still give rise to emergent behavior.

My current AI "framework" now looks like this:

Every soul (it can be anything, a human, an animal, a supermarket) has a set of Desires.
Each desire has a score and may produce an Action.
Every few ticks, each soul selects the desire with the maximal score and applies its action.

pub trait Desire<T>: Send + Sync {
    fn score(&self, goria: &Egregoria, soul: &T) -> f32;
    fn apply(&mut self, goria: &Egregoria, soul: &mut T) -> Action;
}

pub enum Action {
    DoNothing,
    GetOutBuilding(PedestrianID, BuildingID),
    GetInBuilding(PedestrianID, BuildingID),
    GetOutVehicle(PedestrianID, VehicleID),
    GetInVehicle(PedestrianID, VehicleID),
    Navigate(Entity, Itinerary),
    Park(VehicleID, ParkingSpotID),
    UnPark(VehicleID),
    Buy {
        buyer: SoulID,
        seller: SoulID,
        trans: Transaction,
    },
}

Because score and apply take a &Egregoria, it means I can update all souls in parallel.
Rayon does the work of collecting the Actions into a vec.

As an example of a desire, here's the Work desire to tell a pedestrian to go to work in a certain time interval (around 8-18h):

pub struct Work {
    workplace: BuildingID,
    work_inter: RecTimeInterval,
}

impl Work {
    pub fn new(workplace: BuildingID, offset: f32) -> Self {
        Work {
            workplace,
            work_inter: RecTimeInterval::new(
                (8, (offset * SECONDS_PER_HOUR as f32) as i32),
                (18, (offset * SECONDS_PER_HOUR as f32) as i32),
            ),
        }
    }
}

impl Desire<Human> for Work {
    fn score(&self, goria: &Egregoria, _soul: &Human) -> f32 {
        let time = goria.read::<GameTime>();
        0.5 - self.work_inter.dist_until(time.daytime) as f32 * 0.01
    }

    fn apply(&mut self, goria: &Egregoria, soul: &mut Human) -> Action {
        soul.router
            .go_to(goria, Destination::Building(self.workplace))
    }
}

If you want to see this in action, I made a video earlier in september:
https://youtu.be/mfvAuvC-XLg

This concluded my 0.2 milestone to have pedestrians taking their car to go to work and back home 🎉.

Expected it to take 2 or 3 months when I started the project, and 8 months later here we are. :D

Economy

Having a basic economy to guide and analyze the patterns human forms when living is important.
For now, actors like humans or supermarkets can buy and sell goods, but it's not circular or anything.
I will probably have to buy some books on the subject to try and make it interesting.

Day/Night cycle

Since I introduced time to make schedules such as "Get home from 19 -> 7 and work the rest of the time", I felt it was important to show the progress of time using a day/night cycle.

It has a few components:
The ground is lit using normals calculated from the slope of the ground and lit using a sun circling the earth (even though it is flat, which gives weird results when zoomed out).

The roads have some street lamps along the way. The lights are added and then multiplied by the base color, with some ambiant light.

Here's a screenshot at night:

And here's an animation of the day/night cycle:

What next ?

I plan on focusing on getting a functional economy running.
Such as the classic producers -> industry -> commercial -> residents.

Since I did a lot of graphics work, I'm also looking at creating realistic roofs for the houses, but it's hard to be convicing.

Thank you for reading to the end!

< Back to list