Created
October 7, 2023 21:35
-
-
Save kkolyan/f74bdfbc70a88b419401fe5ca9e2a31b to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::cmp::min; | |
use std::f32::consts::PI; | |
use std::ops::{Deref, DerefMut}; | |
use godot::prelude::*; | |
use godot::engine::{CharacterBody3D, CharacterBody3DVirtual, CollisionShape3D, Engine, PhysicsDirectSpaceState3D, PhysicsServer3D, PhysicsShapeQueryParameters3D}; | |
#[derive(GodotClass)] | |
#[class(base = Resource, init)] | |
pub struct UnifiedCharacterConfig { | |
#[init(default = 0.5)] | |
#[export(range = (0.0, 1.0))] | |
pub back_speed_factor: f32, | |
#[init(default = 10.0)] | |
#[export] | |
pub move_speed: f32, | |
// in full-turn per second | |
#[init(default = 1.0)] | |
#[export(range = (1.0, 10.0))] | |
pub turn_speed: f32, | |
#[init(default = true)] | |
#[export] | |
pub pushable: bool, | |
/* | |
class_name UnifiedCharacterConfig | |
extends Resource | |
@export_range(0, 1) var back_speed_factor: float = 0.5 | |
@export var move_speed: float = 10 | |
## In full-turn per second | |
@export_range(0, 10) var turn_speed: float = 1 | |
@export var pushable = true | |
*/ | |
} | |
#[godot_api] | |
impl UnifiedCharacterConfig {} | |
#[derive(GodotClass)] | |
#[class(base = RefCounted, init)] | |
pub struct UnifiedTransitionBounds { | |
#[export] | |
pub transition: Vector3, | |
#[export] | |
pub seconds: f32, | |
} | |
#[godot_api] | |
impl UnifiedTransitionBounds { | |
#[func(gd_self)] | |
pub fn with_transition_and_seconds(mut this: Gd<Self>, transition: Vector3, seconds: f32) -> Gd<UnifiedTransitionBounds> { | |
{ | |
let mut gd_mut = this.bind_mut(); | |
gd_mut.transition = transition; | |
gd_mut.seconds = seconds; | |
} | |
this | |
} | |
} | |
#[derive(GodotClass)] | |
#[class(base = CharacterBody3D)] | |
pub struct UnifiedCharacter { | |
#[base] | |
base: Base<CharacterBody3D>, | |
#[export] | |
config: Option<Gd<UnifiedCharacterConfig>>, | |
space_state: Option<Gd<PhysicsDirectSpaceState3D>>, | |
} | |
#[godot_api] | |
impl CharacterBody3DVirtual for UnifiedCharacter { | |
fn init(base: Base<CharacterBody3D>) -> Self { | |
UnifiedCharacter { | |
config: None, | |
space_state: None, | |
base, | |
} | |
} | |
fn ready(&mut self) { | |
if !Engine::singleton().is_editor_hint() { | |
self.space_state = Some(self.base | |
.get_world_3d().unwrap() | |
.get_direct_space_state().unwrap()) | |
} | |
} | |
fn physics_process(&mut self, _delta: f64) { | |
UnifiedCharacter::ensure_confort_zone(self) | |
} | |
} | |
#[godot_api] | |
impl UnifiedCharacter { | |
/// Called of this method knows nothing about speed capabilities of the character, so supplies both time and distance bounds. | |
/// After the method finished, bounds are updated with remaining time or distance, to allow caller take real move into account | |
#[func] | |
fn move_unified(&mut self, mut bounds: Gd<UnifiedTransitionBounds>) { | |
let mut body = self.base.clone(); | |
let mut bounds = bounds.bind_mut(); | |
let config = self.config.clone(); | |
let config = config.unwrap(); | |
let config = config.bind(); | |
assert_eq!(body.get_rotation().x, 0.0); | |
assert_eq!(body.get_rotation().z, 0.0); | |
let mut body_direction = Quaternion::from_euler(body.get_rotation()) * Vector3::FORWARD; | |
let mut angle = bounds.transition.angle_to(body_direction); | |
let mut real_move_mag = (f32::clamp(real::cos(angle), 0.0, 1.0) + config.back_speed_factor) | |
/ (1.0 + config.back_speed_factor); | |
assert!(real_move_mag <= 1.0, "real_move_mag: {}", real_move_mag); | |
let mut final_speed = config.move_speed * real_move_mag; | |
let mut max_transition = bounds.transition.normalized() * final_speed * bounds.seconds; | |
let mut required_seconds = bounds.transition.length() / final_speed; | |
let mut final_transition; | |
let mut final_seconds; | |
if required_seconds < bounds.seconds { | |
final_seconds = required_seconds; | |
bounds.seconds -= final_seconds; | |
final_transition = bounds.transition; | |
bounds.transition = Vector3::ZERO; | |
} else { | |
final_seconds = bounds.seconds; | |
bounds.seconds = 0.0; | |
final_transition = max_transition; | |
bounds.transition -= final_transition; | |
} | |
body.set_velocity(final_transition.normalized() * final_speed); | |
let signed_angle = body_direction.signed_angle_to(final_transition, Vector3::UP); | |
let mut signed_angle_sign = real::sign(signed_angle); | |
if !utilities::is_equal_approx(real::abs(signed_angle) as f64, angle as f64) { | |
signed_angle_sign *= -1.0; | |
} | |
body.rotate_y(signed_angle_sign * f32::min(angle, config.turn_speed * PI * 2.0 * final_seconds)); | |
body.move_and_slide(); | |
} | |
fn ensure_confort_zone(&mut self) { | |
let mut params = PhysicsShapeQueryParameters3D::new(); | |
params.set_collision_mask(self.base.get_collision_layer()); | |
let name = self.base.get_name().to_string(); | |
let children = self.base.get_children().iter_shared() | |
.map(|it| it.get_name().to_string()) | |
.collect::<Vec<_>>(); | |
let as_node = self.base.get_node("CollisionShape3D".into()); | |
params.set_shape(self.base.get_node_as::<CollisionShape3D>("CollisionShape3D").get_shape().unwrap().upcast()); | |
params.set_transform(Transform3D::new(Basis::IDENTITY, self.base.get_position())); | |
params.set_exclude(Array::from(&[self.base.get_rid()])); | |
let mut collisions = self.space_state.as_mut().unwrap().intersect_shape(params); | |
for collision in collisions.iter_shared() { | |
let other = &mut collision.get("collider").unwrap().try_to::<Gd<UnifiedCharacter>>().unwrap(); | |
let mut collider = other.bind_mut(); | |
if !collider.config.as_mut().unwrap().bind_mut().pushable { | |
continue | |
} | |
let comfort_radius = 1.0; | |
let desired_secs = 0.1; | |
let mut me_to_them = collider.base.get_position() - self.base.get_position(); | |
me_to_them.y = 0.0; | |
let mut penetration = comfort_radius - me_to_them.length(); | |
self.print_by(format!("penetration: {}", penetration).to_variant()); | |
if penetration > 0.0 { | |
collider.base.set_velocity(me_to_them.normalized() * penetration / desired_secs); | |
} | |
collider.base.move_and_slide(); | |
} | |
} | |
#[func] | |
fn print_by(&self, msg: Variant) { | |
godot_print!("{}: {}", self.base.get_name(), msg) | |
} | |
/* | |
static func find_in_precestors(node: Node3D) -> UnifiedCharacter: | |
let mut n = node | |
while n: | |
if n is UnifiedCharacter: | |
return n | |
n = n.get_parent() | |
return null | |
*/ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment