This Week In Veloren 48

14 minute read 30 December 2019
banner

It's the last blog of 2019! With the year wrapping up, we take a look at some of the progress we've made. We've seen many new contributors join Veloren's development, as well as many milestones reached. @Adam also does a deep dive into character states and concepts surrounding them.

- AngelOnFira, TWiV Editor

Contributor Work

Thanks to this week's contributors, @AngelOnFira, @imbris, @Pfau, @Acrimon, @DylanKile, @Mckol, and @Songtronix!

@Tub has been working on a new website layout, which can hopefully be implemented fully in the next few weeks. @Sharp has been continuing work on erosion and has just added debris flow erosion. @Mckol added an Airshipper AUR package for easier installation in Arch. There was also a team meeting, as the release date for 0.5 is coming up!

@Sharp's new debris flow erosion system

2019

With 2019 coming to an end, it's important to look back at what we've accomplished. The images in this edition of TWiV reflect some major milestones we've hit. It's also a great way to see where we started, with just the bare-bones of the new engine. Of course, we should also remember the great contributors that have made this possible!

Note: this just counts contributors that committed to the Gitlab repo. Many others have given input at meetings, created concept art, music, and stories. Big <3 to all of them as well!

(817) Joshua Barretto, (275) timokoesters, (162) imbris, (146) Forest Anderson, (139) jshipsey, (127) Pfau(90), (104) Acrimon, (78) imbris, (70) Pfau, (65) Monty Marz, (55) Marcel, (47) Piotr Korgól, (45) Louis Pearson, (42) Timo Koesters, (40) Joshua Yanovski, (31) Marcel Märtens, (26) Louis Pearson, (24) sxv20_, (23) Songtronix, (21) tommy, (20) Shane Handley, (20) Yeedo, (19) Cody, (17) Songtronix, (16) Vechro, (15) Forest, (15) Mckol, (14) Dominik Broński, (14) Songtronix, (14) telastrus, (13) Songtronix, (13) scott-c, (12) haslersn, (12) robojumper, (11) Imberflur, (9) KyoZM, (9) liids, (9) scorpion(9979), (8) Andrew Pritchard, (8) Justin Shipsey, (7) Adam Fogle, (7) Christoffer Lantz, (6) Joseph Gerardot, (6) S Handley, (5) Cedric Hutchings, (5) Sheldon Knuth, (5) Sheldon Knuth, (5) soruh, (4) Adam Whitehurst, (4) Ahmed Ihsan Tawfeeq, (4) Maciej Ćwięka, (3) Cody, (3) JMS, (3) Tom Watson, (3) Wu Yu Wei, (3) stevedagiraffe, (3) telastrus, (2) Arash Outadi, (2) Demonic, (2) Geno, (2) J. R. Vidal F, (2) Nikita Puzankov, (2) Piotr, (2) Tesseract, (2) Treeco, (2) Vsevolod Timchenko, (2) kacper, (1) Adam Whitehurst, (1) Adam Whitehurst, (1) Algorhythm, (1) Artem, (1) Brian Lewis, (1) Carbonhell, (1) Christian Authmann, (1) Christoffer Lans, (1) Dylan Kile, (1) Erocs, (1) Isaac Freund, (1) Jessie Mancer, (1) Luc Fauvel, (1) Luc Fauvel, (1) MidgeOnGithub, (1) Nero, (1) Nick(12), (1) Nicolas, (1) RustyBamboo, (1) Sebastian Venter, (1) Tanner Donovan, (1) Thomas JULLIEN, (1) appcrashwin(7), (1) heydabop, (1) neronim66@gmail.com, (1) pestilence, (1) yashaslokesh, (1) yeet

This year's credits are brought to you by git shortlog -sne --since="1 year ago"

One of the oldest videos of the new engine (March 4th)

Character States Overhaul by @Adam

Hello, @Adam here! I'm working on redesigning the Character State System so that it can (hopefully) handle all the awesome ideas that #game-design has in mind. This will include the combat, abilities, and weapons systems of Veloren. I'm going to walk through all the guts of this new system, to illustrate how it works and why I'm so proud of it. :) But first, some background...

The CharacterState component is what handles behaviors of "characters": Any player or NPC, human or otherwise. Each CharacterState actually has two concurrent states: an ActionState which is primarily concerned with how a character is interacting with the world (Attacking, Blocking, Wielding), and a MoveState which primarily describes how a player is moving around the world (Standing, Running, Gliding, Climbing).

The beginning of networking! (March 18th)

Existing State of Character States

When I began, the existing movement, controller, and combat systems were under-equipped to handle the exciting ideas that #game-design was coming up with. Here are a few reasons why.

First, CharacterState update logic was spread across these three separate systems, so to make any changes or fix any bugs, one would first have to understand how all three of these systems worked, just to change one state's behavior.

Second, the controller system was the main source for state-transitioning logic, but that logic was occurring across a series of conditional checks, with state behavior logic mixing with that of others. This meant that changing one state's implementation could have unpredictable effects on another's.

Original concept of character creation screen (March 18th)

Finally, The state interactions during combat were being defined by the combat system itself. For example, when you tried to attack, the system would check if the enemy was blocking, and handled that blocking logic. So to have new, interesting ways of attacking/blocking/dodging, we would have to layer that on top of the logic of every other attack/block/dodge behavior.

What needed to happen instead was that the blocking state needed to be able to define how it would handle blocking an attack i.e: Would the block interrupt the attacker? Would it apply a strength debuff?

The state of graphics in Veloren with AO, FXAA, and terrain gradients (May 6th)

To summarize, the issues with the existing systems are:

  1. State updating logic was spread across three separate, interlocked systems leading to cognitive overhead.
  2. State behavior logic depended on the behavior logic of every other state. Changing one state would affect every other state in unpredictable ways.
  3. State handling was too static and would not scale as weapons and abilities were added.

Playing around with NPC characters (May 13th)

Veloren Character State System (VCSS)

With these issues in mind, I'm going to start at the root of this new Veloren Character State System (referred to simply as 'VCSS'). Peek into the implementation file: sys/character_state.rs. Don't worry! This part is actually pretty simple, mostly just boilerplate!

The key piece of code is this:

// Determine new move state if character can move
if let (None, false) = (move_overrides.get(entity), character.move_disabled) {
 let state_update = character.move_state.handle(&EcsStateData {
 entity: &entity,
 uid,
 character,
 pos,
 vel,
 ori,
 dt: &dt,
 inputs,
 stats,
 body,
 physics,
 updater: &updater,
 server_bus: &server_bus,
 local_bus: &local_bus,
 });

 *character = state_update.character;
 *pos = state_update.pos;
 *vel = state_update.vel;
 *ori = state_update.ori;
}

The character's action_state update logic is similar, so we only need to focus on move_state.

First, the code must check whether the character's MoveState can update. The character's ActionState may override the MoveState, or there may be some outside override occurring from an OverrideMove component.

If it can, the VCSS packages all the components that any state might need to read into an EcsStateData struct, and passes that to move_state.handle(). More on this function later.

The new shadow system (June 3rd)

What's so nice about this EcsStateData struct is: In the future, a newly implemented state might need to read a component that doesn't currently exist or isn't needed for current states. That new component can be plugged into the EcsStateData struct, without needing to update the parameters of all the existing states. They will accept (and be able to read) that new component, through EcsStateData automatically.

Notice, though, that this function returns something labeled state_update. Well, that's the StateUpdate tuple, which is really just a tuple of new CharacterState, Pos, Vel, and Ori components, that the current state has determined should be the updated state of the components.

This illuminates a very critical part of the new VCSS. State update handle() functions are pure. They cannot mutate any components they are passed, they must return new components. The reason for this is twofold:

  1. State handle() functions only read data, and return new data, so they can be easily parallelized.
  2. ECS data-behavior-constraint is maintained, systems handle behavior, and the VCSS here still handles manipulating the data in storage.

Alright, I bet you thought we'd move onto move_states handle() function now. Not yet, but I appreciate your enthusiasm ;) First, we must know where this handle() function comes from (and why).

Town generation (September 2nd)

Meet StateHandle trait

It's defined in common/src/comp/states/mod.rs:

pub trait StateHandle {
 fn handle(&self, ecs_data: &EcsStateData) -> StateUpdate;
}

This guy is the glue that connects the VCSS to a state and its update logic. It takes in that EcsStateData struct, and passes back a StateUpdate tuple like I just explained. Knowledgeable observers might be asking: "Wait... but you just said ECS systems need to separate data and behavior, but you're implementing functions on the data to handle how to update that data?"

As a newbie to the Rust language, this was a problem that made me question my entire implementation strategy (and worth as a programmer, but that's for my therapist).

In case you are unfamiliar with why ECS systems are so powerful and why this was so troubling: The slowest part of game programs is usually waiting for data to be moved between cache and storage. Data-behavior-separation makes ECS systems so efficient because only relevant data is cached when behaviors are performed. Traditional Object-Oriented data-behavior-encapsulation caches the relevant data, along with all the other data (and behavior functions) that an object has, which is just wasted space in the cache. This leads to more calls to storage, and major performance hits.

Rivers and erosion (September 30th)

The key takeaway is that we want to minimize calls to storage by caching only relevant data. Bundling data up with their behavior would take up excess space in the cache!

So, I spent a long time looking in the mirror and asking, "Is this what we want? Is this the right approach? What am I missing about ECS?" Of course, after wasting so much time in the bathroom, I did a little research and found that missing piece: trait member functions are syntactic sugar for static functions that accept their implementors as the first argument. And that was a sweet discovery indeed. :)

What this means is that calling move_state.handle() is the same as calling MoveState::handle(move_state). The function implementation is not actually bundled into the state struct, it exists somewhere outside the struct, as a static function, just like the VCSS that called it. It's not put into cache along with the data, and therefore won't waste space! The VCSS can still churn through only relevant data at lightning speed and us dumb humans can move that handle() function somewhere else where it's easier to manage! Win-win.

Handling StateHandle

So, states impl StateHandle trait to define how their data gets updated, but then you might notice something else! MoveState isn't actually a state, it's the enum that defines all the possible MoveStates! So why does it impl StateHandle?

Well, fictitious reader-in-my-head, you are quite astute. This is another reason why StateHandle is the glue between the VCSS and states. Peek below StateHandles definition at the impl StateHandle for MoveState and you will see that its just a big ol' match statement that passes EcsStateData to the handle() functions of the states themselves.

Moon and clouds (November 18th)

This was a design decision that I think is important. While this matching could have been done within the VCSS itself, I chose to put it here because...

  1. It keeps the VCSS concise
  2. This matching logic is an implementation detail of how states handle their updates and not a detail of the VCSS itself.
  3. There are several RFC's and plans for rust to eliminate the need for matching on enum variants just to perform the same trait function that is implemented by all variants of that enum. Thus, it could likely be refactored away in the future.
  4. You have to update states/mod.rs when implementing new states anyway, so now we don't need to touch the VCSS file every time we want to add a new state.

Okay, I'm done patting myself on the back. Before I get to the stuff you care about, I should explain this nest of matches inside impl StateHandle for ActionState, because this was a stumbling point for me as a newbie rustacean:

/// Passes handle to variant or subvariant handlers
fn handle(&self, ecs_data: &EcsStateData) -> StateUpdate {
 match self {
 Attack(kind) => match kind {
 BasicAttack(state) => state.handle(ecs_data),
 Charge(state) => state.handle(ecs_data),
 },
 Block(kind) => match kind {
 BasicBlock(state) => state.handle(ecs_data),
 },
 Dodge(kind) => match kind {
 Roll(state) => state.handle(ecs_data),
 },
 Wield(state) => state.handle(ecs_data),
 Idle(state) => state.handle(ecs_data),
 // All states should be explicitly handled
 // Do not use default match: _ => {},
 }
}

So we're matching on self (an ActionState)... then if its an Attack, we match on kind (an Actionkind), and ActionKind variants have some object inside them called state that has the handle() we want to call. Cool. Why?

Let's look at BasicAttack. It holds a BasicAttackState struct that we will see defined in a moment. This struct defines all the data fields that BasicAttack needs, along with the implementation of StateHandle trait, because enum variants aren't allowed to impl traits (yet). BasicAttack is an AttackKind itself. We have different AttackKind variants inside the Attack ActionState variant because this allows us to easily check character states by different levels. Sometimes we only need to check if a character is attacking, sometimes we want to know what attack they are using. Most importantly, if different attacks were added to ActionState enum itself, every additional state would require updating other systems that match on action state but don't care about what kind of attack the character is using.

Okay, dear reader, I think it's time ...

Where it All Comes Together

Take a lil' looksee at common/src/comp/states/basic_attack.rs. Inside you'll find nothing surprising. You've got the BasicAttackStruct I already talked about:

pub struct BasicAttackState {
 /// How long the state has until exiting
 remaining_duration: Duration,
}

With a field, remaining_duration, so the state knows when it should finish. This is all the data that's in this state that's going into cache.

'impl StateHandle' trait won't go into cache. It's handle function initializes new components:

let mut update = StateUpdate {
 pos: *ecs_data.pos,
 vel: *ecs_data.vel,
 ori: *ecs_data.ori,
 character: *ecs_data.character,
};

It does some behavior stuff, if the remaining_duration is 0, it goes back to wielding or idle state, otherwise, it ticks down remaining_duration. Either way, it just returns the updated components. :) That's it, that's how a full state is implemented, from start to finish.

But let's just take a moment to appreciate what's going on here. All of a single state's implementation, data, and behavior, looks like it's in one place, so it's easy to see all the important information us humans need when writing states, but the program can still separate this into its data and behavior pieces for ECS. What's more, you don't see any of the logic for anything that isn't part of BasicAttackState. You don't need to know how the VCSS calls this state update, you don't need to know what other state behavior looks like, you don't need to rely on a different movement systems to move the character, you don't have to worry if this state will break parallelization in the system, none of that junk! All you have in front of you is the state you care about right now, and that's the way it should be. :)

So, with all that ogling and awing, I encourage you to look at other states' implementations and see how they work. Also, check out common/src/util/movement_utils.rs to see what those functions can do for you, you don't have to rewrite logic that is shared across states, do you? And don't be afraid to reach out to me with questions, suggestions, or to volunteer! ;)

That's all for this year! Catch you in 2020 :)