API design for special movement actions #24
Replies: 9 comments 27 replies
-
Initiating air actionsThe instinctive way to design an API for a double jump is to add a
Tnua aims to maximize customizability, and the way to do it is to move those decisions to user systems. This means that Tnua itself will not know how many jumps the player is allowed to do midair. Instead, Tnua will provide a mechanism for the user system to determine the number of the next jump, and the user system will then select whether or not to jump and with which strength: // Very pseudocode
controls.jump = if jump_button_pressed() {
match next_midair_jump_number() {
0 => Some(1.0), // jump from the ground
1 => Some(0.6), // air jump is less powerful
2 => Some(0.2), // Second air jump is even tinier
_ => None // third air jump is not allowed
}
} else {
None
} |
Beta Was this translation helpful? Give feedback.
-
Types of air actionsTnua already has jumps, and the air version should work the same as he ground action - the controls will specify the jump's height, and Tnua will calculate the initial vertical velocity required to reach that height. How would the API for the other types of actions work? Actions that I can think of:
Note that many of these interact with the environment. My vision is that some other mechanism will tell the user control system how the environment looks like, and then it would create environment-unaware air actions. These are a lot of actions, and I want to see if I can generalize them somehow. |
Beta Was this translation helpful? Give feedback.
-
Actions that use the environmentThis is about actions like wall jumping, ledge hanging, vaulting over obstacles, etc. My direction for design is that the actions themselves will not be aware of the environment geometry itself - Tnua will process a wall jump action the same even if there is no wall there. Instead, I'll provide a It'd look something like that (as always - very pseudo. And the rest of the API may not work as depicted here): if player_is_airborne() && player_is_pushing_jump() {
if let Some(direction) = direction_player_is_pushing() {
if let Some(surface) = spatial_sensor.surface_at_direction(direction) {
control.action = Some(TnuaAction::WallJump {
jump_height: 1.0,
wall_position: surface.position,
wall_normal: surface.normal,
});
}
}
} If a similar action was sent without a wall being there, Tnua would have made the character jump off an imaginary wall. It should not need the wall itself - only the data provided about it inside the action. |
Beta Was this translation helpful? Give feedback.
-
Multiple actions at onceWhat should happen if the user control system invokes multiple action motions at once? Should this be allowed? Won't the actions interfere with each other? |
Beta Was this translation helpful? Give feedback.
-
Design idea - composition by axisMaybe the actions will be more flexible if they can declare individual behavior on up to 3 axes? A wallslide action, for example, will look like this: actions.set([
TnuaActionComponent {
project_on: TnuaProjection::Plane { normal: Vec3::Y },
operation: TnuaActionOperation::FixtateTranslation(position_where_the_character_slides),
..Default::default()
},
TnuaActionComponent {
project_on: TnuaProjection::Axis { direction: Vec3::Y },
operation: TnuaActionOperation::CapVelocity {
direction: -Vec3::Y, // required despite the axis
max_speed: 2.0, // this is the max slide speed. It's positive, but the direction is negative
},
max_acceleration: 5.0, // TBD - is this in addition to gravity?
..Default::default()
},
]); Any axes not covered by a |
Beta Was this translation helpful? Give feedback.
-
The polymorphism solutionThe action will be a trait object that accepts the action-less motor output (plus configuration, sensor-input, and custom state?) and can modify it. This means that each action will have to be written manually, but I can just provide a library of helpers for all the common actions. |
Beta Was this translation helpful? Give feedback.
-
ECS approachSimilar to the polymorphism approach, but uses ECS - modify the motor decision from user systems. While I'm usually all for ECS solutions, in this case I don't like it:
I think some of these problems can be avoided by spawning a new entity for the action. But that's still more overhead than the polymorphic approach. |
Beta Was this translation helpful? Give feedback.
-
Thank you for your knowledge |
Beta Was this translation helpful? Give feedback.
-
Concrete API suggestion (polymorphic actions, jump is just another action, unified with action counter)controls.set_action(TnuaSetAction {
identifier: "jump",
action: tnua_actions::Jump(20.0), // move the height from the config to here, since it's no longer privileged?
allowed: TnuaActionAllowed::Grounded {
buffer: 0.2,
},
}); This is a bit cumbersome, so maybe a fluent interface would be better: // The default `allowed` is Grounded { buffer: 0.0 }
controls.set_action("jump", tnua_actions::Jump(20.0)).allow_buffer(0.2); The identifier can be used to determine if the user switch action, to handle multiple action inputs (not combine them - just determine which one was the last to start), and to write in the animating output so that another system could handle the animation for the action. While these can be done with the
It'll work like this: // Must be called before setting actions this frame. Or maybe just call it
// `begin_frame()` and have it reset the movement vector as well?
controls.action_frame_begin();
if dash_input() {
controls.set_action("dash", tnua_actions::Dash(...));
}
if jump_input() {
controls.set_action("jump", tnua_actions::Jump(20.0));
}
As for
Initially I thought about also having a controls.begin_frame();
if tnua_state.is_grounded() {
*jumps_counter = 0;
} else {
match tnua_state.action_report() {
TnuaActionReport::Started("jump") => {
*jumps_counter += 1;
},
TnuaActionReport::Ongoing("jump") => {
// Can also do stuff in this case, but not this time
},
_ => {}
}
// Player stepped from a cliff
if jumps_counter == 0 {
jumps_counter = 1;
}
}
match jumps_counter {
1 => {
// Air jump is not as powerfull as regular jump
controls.set_action("jump", tnua_actions::Jump(10.0)).allow_always();
}
2 => {
// Second air jump is even less powerful
controls.set_action("jump", tnua_actions::Jump(5.0)).allow_always();
}
_ => {
// Note that this **is** the regular jump. If the player tries to jump
// a third time in the air it won't jump and instead buffer a jump in
// case they reach the ground soon enough after.
controls.set_action("jump", tnua_actions::Jump(20.0)).allow_buffer(0.2);
}
} (note that |
Beta Was this translation helpful? Give feedback.
-
I want to figure out the design for things like double jumps, air dashes, wall jumps, vaulting, gliding, etc...
It feels like these should all be connected. A wall jump is like a double jump, but when a wall is nearby. And maybe with an horizontal boost? But an horizontal boost is a dash. And the mechanism for detecting when an air dash is allowed should be connected to the mechanism for detecting when a double jump is allowed.
So I want to come up with a good design that takes them all into account.
Beta Was this translation helpful? Give feedback.
All reactions