#How to best support simulating/rendering an entity that has two different perspectives?

25 messages · Page 1 of 1 (latest)

exotic jasper
#

Hi! This is like 50% me asking for help and 50% me just rubberduck ing a bit.

I am creating a game which is similar to SimAnt. It has two main perspectives for viewing ants: a side-view perspective of an ant nest and a tilted, top-down perspective of ants on the surface.

I am trying to figure out how to best represent the concept of orientation.

I built the side perspective first and am now trying to retrofit my logic to support the top-down perspective. The way I implemented orientation for the side perspective is:

#[derive(Component, Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Reflect, Default)]
#[reflect(Component)]
pub struct AntOrientation {
    facing: Facing,
    angle: Angle,
}

#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Reflect, Default)]
pub enum Facing {
    #[default]
    Left,
    Right,
}

#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize, Reflect, Default)]
pub enum Angle {
    #[default]
    Zero,
    Ninety = 90,
    OneHundredEighty = 180,
    TwoHundredSeventy = 270,
}

All permutations of facing and angle are valid for the side perspective. Ants can walk on the ground, the walls, or even upside down. They can also face the left or right. This means there are 8 different ways to rotate/flip a single sprite. My first screenshot shows a few examples.

Now, the top-down perspective is a little different. There aren't 8 permutations - just 4. An ant could be facing up/down/left/right. An ant facing left/right is identical to an ant facing/left with an Angle of Zero in the side-view perspective, but the parallels end there.

An ant facing Up on the top-down perspective is sort of like an ant turned Ninety degrees, but with neither Left nor Right facing.

So, this begs the question, how to best go about supporting this? I feel there are a few options:

  1. Continue using AntOrientation. Introduce Facing::Up and Facing::Down which are only applicable to the top-down perspective and acknowledge that only Angle::Zero is applicable to the top-down perspective.
  2. Introduce distinct components for each perspective: AntSideOrientation AntTopOrientation. This would allow me to more clearly represent valid states, but invalidate a lot of helper functions.

I feel like I am naturally inclined to take the second approach, but it makes life a lot harder and also feels a little weird because both perspectives are a 2D tilemap.

For example, I have an "ant hunger" system which encourages ants to eat by checking the location ahead of them for food. This requires knowing orientation. I don't really want to create separate hunger systems for each zone just to support distinct orientations. The system works functionally equivalent if an ant is facing "up" or if its angled "90 degrees, facing left/right" - in both scenarios one tile up on the y-axis is the target position.

Similarly, I have a custom command, spawn_ant, which expects AntOrientation. I would need to change this to accepting one of AntSideOrientation or AntTopOrientation which starts to feel like a code smell.

fn spawn_ant<Z: Zone>(
    &mut self,
    position: Position,
    color: AntColor,
    orientation: AntOrientation,
    inventory: AntInventory,
    role: AntRole,
    name: AntName,
    initiative: Initiative,
    zone: Z,
);

So yeah, I guess I am a bit torn. Cramming both concepts into my existing AntOrientation feels easier to get started with, but more difficult to maintain and reason about. It feels more "ECS-y" to swap components in/out when not relevant, but difficult to reuse generic systems when I'm making such granular distinctions.

#

Here is my hunger system. As we can see, it only really depends on orientation to answer the question "Which tile position is directly in front of the ant's head?"

pub fn ants_hunger_act<Z: Zone + Copy>(
    mut ants_hunger_query: Query<
        (
            Entity,
            &Hunger,
            &mut Digestion,
            &AntOrientation,
            &Position,
            &mut AntInventory,
            &mut Initiative,
            &Z,
        ),
        With<Z>,
    >,
    grid_elements: GridElements<Z>,
    elements_query: Query<&Element, With<Z>>,
    mut commands: Commands,
    mut ant_ate_food_event_writer: EventWriter<AntAteFoodEvent>,
) {
    for (
        ant_entity,
        hunger,
        mut digestion,
        orientation,
        position,
        mut inventory,
        mut initiative,
        zone,
    ) in ants_hunger_query.iter_mut()
    {
        if hunger.is_starved() {
            commands
                .entity(ant_entity)
                .insert(Dead)
                .remove::<Initiative>();
        } else if hunger.is_peckish() {
            if !initiative.can_act() {
                continue;
            }

            // If there is food near the hungry ant then pick it up and if the ant is holding food then eat it.
            if inventory.0 == None {
                let ahead_position = orientation.get_ahead_position(position);
                if grid_elements.is(ahead_position, Element::Food) {
                    let food_entity = grid_elements.entity(ahead_position);
                    commands.dig(ant_entity, ahead_position, *food_entity, *zone);
                }
            } else {
                let element = elements_query.get(inventory.0.unwrap()).unwrap();

                if *element == Element::Food {
                    inventory.0 = None;

                    digestion.increment(-0.20);
                    initiative.consume();

                    ant_ate_food_event_writer.send(AntAteFoodEvent(ant_entity));
                }
            }
        }
    }
}

I guess this is sort of where querying on a trait which implements get_ahead_position would be beneficial

#

I guess I could go deeper into generics

#

like if I had Orientation<TopDown> and Orientation<Side> and fulfilled that when setting up ants_hunger_act for each zone

#

or even just reused Zone

#

🤔

#

maybe that's the key

#

yeah I think that's the key

exotic jasper
#

Well, maybe lol, i'll play with it a bit

#

going to look at how all the time/time<fixed><virtual> stuff works for ideas

exotic jasper
#

I thiiiink that maybe those approaches are a little more specialized

#

I'm just going to hack at it to start and create separate components entirely and see where I end up in similarity afterward

wide sandal
#

I think I would make an enum with all the possible orientations (north, south, east, west, up, down) and forget about angle if you can (I'm not sure if I get what that's for). Then I would make another enum for the possible positions of the ant, something like:

enum AntLocations {
Underworld(Orientation),
Overworld(Orientation),
}

And using methods I would make sure that you can never have an orientation that doesn't match the location. I havent' tried any of this, just intuition about how I would solve it, what do you think?

exotic jasper
#

angle is used for the side-perspective to represent stuff like an ant walking upside down on a ceiling

#

I do think the enum is a good idea and is where I am leaning

#

lemme draw a crude picture though 1 sec

#

I don't think it's possible to simplify this to a representation that is consistent with a 4-axis top-down perspective

#

but maybe I am missing something!

#

The difference though is that feet have to have stable footing when considering a side perspective in 2D space, but feet implicitly have stable footing when considering a top-down perspective in 2D space

#

my north/south sprites still suck sorry but here we see that there's only 4 directions in top-down

#

I think my first attempt at this is going to be not great but will share the code when I have it working at least

wide sandal
#

Ah, I see what you mean, you need multiple versions of left-right and north-south

#

Alright, another idea. It means building something like a quaternion, but adapted to your case. Again you have only one enum with six orientations:

enum Orientation { North, East, South, West, Up, Down }

Then each ant has a Position component with two fields:

struct Position { facing: Orientation, normal: Orientation }

The "normal" value represents the direction of the normal of the surface the ant is resting on. When the ant is outside this value is always "Up". When inside, it depends on the surface it is resting on. Your 90 deg left would become Up/East, and your 270 deg, left would be Up/West. 180 deg right would be East/Down and 180 deg left would be West/Down. I think that covers everything, and simplifies it but not needing a number (if there are only four possible values for the angle is better to use an enum than an angle, I would say)

exotic jasper
#

I think ultimately I decided to not go in a direction that merges the two concepts together. It seems better to keep them distinct because, for example, if I want to know all valid permutations for a given zone then it becomes a bit tedious to tease out which values correlate to which zone.