Skip to content

Instantly share code, notes, and snippets.

@kkolyan
Created October 7, 2023 21:35
Show Gist options
  • Save kkolyan/f74bdfbc70a88b419401fe5ca9e2a31b to your computer and use it in GitHub Desktop.
Save kkolyan/f74bdfbc70a88b419401fe5ca9e2a31b to your computer and use it in GitHub Desktop.
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