Last active
April 23, 2020 00:59
-
-
Save arthurmco/92ab10eb55cbc332132010bb5b46502a to your computer and use it in GitHub Desktop.
Core structures of Familyline logic engine in Rust
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::sync::Arc; | |
/// Familyline test for a possible Rust rewrite | |
#[derive(Debug, Copy, Clone)] | |
pub struct Vec3 { | |
x: f32, | |
y: f32, | |
z: f32, | |
} | |
/// A dummy mesh object | |
#[derive(Debug)] | |
pub struct MeshObject {} | |
/// Stores information about who represents this entity | |
/// on screen | |
pub trait LocationComponentT: core::fmt::Debug { | |
fn get_mesh(&self) -> Option<&MeshObject>; | |
fn get_mesh_mut(&mut self) -> Option<&mut MeshObject>; | |
fn update_mesh(&mut self, m: MeshObject); | |
} | |
/// Attack information table | |
/// | |
/// Essentially, the HP lost is the attacker's attack minus the defender armor. | |
#[derive(Debug, Clone)] | |
pub struct AttackInformation { | |
/// Amount of damage dealt, per tick | |
attack: f32, | |
/// Amount of armor blocked, per tick | |
armor: f32, | |
} | |
/// Why one object can't attack the other | |
pub enum CanAttackFailReason { | |
/// The defender is out of range | |
OutOfRange, | |
/// The defender cannot be attacked because it does not have an | |
/// attack component. | |
NotAttackable, | |
// Both entities are from the same colony | |
SameColonies, | |
// Both entities come from allied colonies | |
AlliedColonies, | |
} | |
/// Stores information about how two entities will attack | |
/// | |
/// If an entity misses this component, it means that the entity | |
/// will not be able to attack, but will not be able to be attacked | |
/// either. | |
pub trait AttackComponentT: core::fmt::Debug { | |
fn get_total_hp(&self) -> u32; | |
/// Get current HP | |
/// | |
/// Floating point to allow any attack that removes less than 1HP to be | |
/// registered and accounted for | |
fn get_current_hp(&self) -> f64; | |
fn get_attack_information(&self) -> &AttackInformation; | |
/// Check if an unit can attack another | |
fn can_attack(&self, defender: &dyn GameEntity) -> Result<(), CanAttackFailReason>; | |
} | |
/// Game entity main trait | |
/// | |
/// All entities should implements the methods below | |
/// | |
/// We implement `Debug` just so that we can see what is inside. | |
pub trait GameEntity: core::fmt::Debug { | |
fn get_id(&self) -> Option<usize>; | |
fn update_id(&mut self, id: usize); | |
fn get_position(&self) -> Option<Vec3>; | |
fn set_position(&mut self, v: Vec3); | |
fn get_location_component(&self) -> Option<&dyn LocationComponentT>; | |
fn get_location_component_mut(&mut self) -> Option<&mut dyn LocationComponentT>; | |
fn get_attack_component(&self) -> Option<&dyn AttackComponentT>; | |
fn get_attack_component_mut(&mut self) -> Option<&mut dyn AttackComponentT>; | |
fn get_name(&self) -> &str; | |
fn get_type(&self) -> &str; | |
fn update(&mut self) {} | |
} | |
///////// | |
#[derive(Debug)] | |
struct DefaultLocationComponent { | |
m: Option<MeshObject>, | |
} | |
impl DefaultLocationComponent { | |
fn new() -> DefaultLocationComponent { | |
DefaultLocationComponent { m: None } | |
} | |
} | |
impl LocationComponentT for DefaultLocationComponent { | |
fn get_mesh(&self) -> Option<&MeshObject> { | |
match &self.m { | |
Some(m) => Some(m), | |
None => None, | |
} | |
} | |
fn get_mesh_mut(&mut self) -> Option<&mut MeshObject> { | |
match &mut self.m { | |
Some(m) => Some(m), | |
None => None, | |
} | |
} | |
fn update_mesh(&mut self, m: MeshObject) { | |
self.m = Some(m) | |
} | |
} | |
////////////// | |
#[derive(Debug)] | |
struct DefaultAttackComponent { | |
info: AttackInformation, | |
total_hp: u32, | |
current_hp: f64, | |
} | |
impl DefaultAttackComponent { | |
fn new(hp: u32, info: AttackInformation) -> DefaultAttackComponent { | |
DefaultAttackComponent { | |
total_hp: hp, | |
current_hp: hp as f64, | |
info, | |
} | |
} | |
} | |
impl AttackComponentT for DefaultAttackComponent { | |
fn get_total_hp(&self) -> u32 { | |
self.total_hp | |
} | |
fn get_current_hp(&self) -> f64 { | |
self.current_hp | |
} | |
fn get_attack_information(&self) -> &AttackInformation { | |
&self.info | |
} | |
fn can_attack(&self, defender: &dyn GameEntity) -> Result<(), CanAttackFailReason> { | |
Err(CanAttackFailReason::NotAttackable) | |
} | |
} | |
/////////////////////// | |
/// The entity manager | |
/// | |
/// Stores, and has absolute ownership of all entities. | |
#[derive(Debug)] | |
struct EntityManager { | |
next_id: usize, | |
entities: Vec<Box<dyn GameEntity>>, | |
} | |
impl EntityManager { | |
fn new() -> EntityManager { | |
EntityManager { | |
next_id: 1, | |
entities: Vec::new(), | |
} | |
} | |
fn count(&self) -> usize { | |
self.entities.len() | |
} | |
/// Add an object, return its ID | |
/// | |
/// We store the ownership to the game entity. | |
fn add(&mut self, mut v: Box<dyn GameEntity>) -> usize { | |
let id = self.next_id; | |
v.update_id(id); | |
self.entities.push(v); | |
self.next_id = id + 1; | |
id | |
} | |
/// Get a reference from an object from its ID | |
fn get(&self, id: usize) -> Option<&Box<dyn GameEntity>> { | |
self.entities.iter().find(|c| { | |
if let Some(oid) = c.get_id() { | |
oid == id | |
} else { | |
false | |
} | |
}) | |
} | |
/// Get a mutable reference from an object from its ID | |
fn get_mut(&mut self, id: usize) -> Option<&mut Box<dyn GameEntity>> { | |
self.entities.iter_mut().find(|c| { | |
if let Some(oid) = c.get_id() { | |
oid == id | |
} else { | |
false | |
} | |
}) | |
} | |
/// Remove an object with the ID `id` from the object manager | |
/// | |
/// You should already have notified the entity lifecycle manager | |
/// about the removal of this component. | |
/// | |
/// We unwrap the ID because the object should have an ID already | |
fn remove(&mut self, id: usize) { | |
self.entities.retain(|e| e.get_id().unwrap() != id) | |
} | |
fn update(&mut self) { | |
for e in &mut self.entities { | |
e.update(); | |
} | |
} | |
} | |
///////////////////////////// | |
/// A generic entity, with no customization besides the update callback | |
/// | |
/// Useful for 99% of the cases. | |
struct GenericEntity<Cb> | |
where | |
Cb: Fn(&mut dyn GameEntity) -> (), | |
{ | |
id: Option<usize>, | |
name: String, | |
entity_type: String, | |
position: Option<Vec3>, | |
lc: DefaultLocationComponent, | |
ac: DefaultAttackComponent, | |
/// The update callback | |
/// | |
/// We use an Arc<> so we can copy the callback from the struct, | |
/// and call it, without having to borrow it from the generic entity | |
/// structure | |
/// | |
/// Since we pass the `self` object to the callback, so it can do | |
/// alterations, it would not compile, because we would borrow it | |
/// twice. | |
update_callback: Arc<Box<Cb>>, | |
} | |
impl<Cb> GenericEntity<Cb> | |
where | |
Cb: Fn(&mut dyn GameEntity) -> (), | |
{ | |
fn new(name: &str, entity_type: &str, update_callback: Cb) -> GenericEntity<Cb> { | |
GenericEntity { | |
id: None, | |
name: String::from(name), | |
entity_type: String::from(entity_type), | |
position: None, | |
lc: DefaultLocationComponent::new(), | |
ac: DefaultAttackComponent::new( | |
100, | |
AttackInformation { | |
attack: 1.0, | |
armor: 0.8, | |
}, | |
), | |
update_callback: Arc::new(Box::new(update_callback)), | |
} | |
} | |
} | |
impl<Cb> GameEntity for GenericEntity<Cb> | |
where | |
Cb: Fn(&mut dyn GameEntity) -> (), | |
{ | |
fn get_id(&self) -> Option<usize> { | |
self.id | |
} | |
fn get_name(&self) -> &str { | |
&self.name | |
} | |
fn update_id(&mut self, id: usize) { | |
self.id = Some(id); | |
} | |
fn get_position(&self) -> Option<Vec3> { | |
self.position | |
} | |
fn set_position(&mut self, v: Vec3) { | |
self.position = Some(v); | |
} | |
fn get_location_component(&self) -> Option<&dyn LocationComponentT> { | |
Some(&self.lc) | |
} | |
fn get_location_component_mut(&mut self) -> Option<&mut dyn LocationComponentT> { | |
Some(&mut self.lc) | |
} | |
fn get_attack_component(&self) -> Option<&dyn AttackComponentT> { | |
Some(&self.ac) | |
} | |
fn get_attack_component_mut(&mut self) -> Option<&mut dyn AttackComponentT> { | |
Some(&mut self.ac) | |
} | |
fn get_type(&self) -> &str { | |
&self.entity_type | |
} | |
fn update(&mut self) { | |
(self.update_callback.clone())(self); | |
println!("Updated"); | |
} | |
} | |
use std::fmt; | |
/// Manually implement the Debug trait, because `Fn`s do not | |
/// have a debug trait implementation. | |
/// We only have to add something generic here | |
impl<Cb> fmt::Debug for GenericEntity<Cb> | |
where | |
Cb: Fn(&mut dyn GameEntity) -> (), | |
{ | |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
f.debug_struct("GenericEntity") | |
.field("id", &self.id) | |
.field("name", &self.name) | |
.field("position", &self.position) | |
.field("lc", &self.lc) | |
.field("ac", &self.ac) | |
.field("update_callback", &String::from("Fn<>")) | |
.finish() | |
} | |
} | |
#[cfg(test)] | |
mod tests { | |
// Note this useful idiom: importing names from outer (for mod tests) scope. | |
use super::*; | |
#[test] | |
fn test_object_add() { | |
let mut em = EntityManager::new(); | |
let de = GenericEntity::new("Test1", "test", |_| ()); | |
let id = em.add(Box::new(de)); | |
assert_eq!(id, 1); | |
let de = GenericEntity::new("Test2", "test", |_| ()); | |
let id = em.add(Box::new(de)); | |
assert_eq!(id, 2); | |
assert_eq!(em.count(), 2); | |
} | |
#[test] | |
fn test_object_add_and_get() -> Result<(), String> { | |
let mut em = EntityManager::new(); | |
let _ = em.add(Box::new(GenericEntity::new("Invalid", "test", |_| ()))); | |
let id = em.add(Box::new(GenericEntity::new("Test0001", "test", |_| ()))); | |
let _ = em.add(Box::new(GenericEntity::new( | |
"AnotherInvalid", | |
"test", | |
|_| (), | |
))); | |
let entity = em.get(id); | |
match entity { | |
Some(e) => { | |
assert_eq!(e.get_id().unwrap(), id); | |
assert_eq!(e.get_name(), "Test0001"); | |
Ok(()) | |
} | |
None => Err(String::from("the entity should be found, it was not found")), | |
} | |
} | |
#[test] | |
fn test_object_call_update() -> Result<(), String> { | |
let mut em = EntityManager::new(); | |
let _ = em.add(Box::new(GenericEntity::new("Invalid", "test", |_| ()))); | |
let id = em.add(Box::new(GenericEntity::new( | |
"TestCallback", | |
"test", | |
|mut e| { | |
e.set_position(Vec3 { | |
x: 10.0, | |
y: 1.0, | |
z: 5.0, | |
}); | |
}, | |
))); | |
let _ = em.add(Box::new(GenericEntity::new( | |
"AnotherInvalid", | |
"test", | |
|_| (), | |
))); | |
em.update(); | |
let entity = em.get(id).unwrap(); | |
match entity.get_position() { | |
Some(pos) => { | |
assert_eq!(pos.x, 10.0); | |
assert_eq!(pos.y, 1.0); | |
assert_eq!(pos.z, 5.0); | |
Ok(()) | |
} | |
None => Err(String::from( | |
"the position should be set from the closure!!!", | |
)), | |
} | |
} | |
#[test] | |
fn test_object_call_update_multiple_times() { | |
let mut em = EntityManager::new(); | |
let _ = em.add(Box::new(GenericEntity::new("Invalid", "test", |_| ()))); | |
let id = em.add(Box::new(GenericEntity::new( | |
"TestCallbackMulti", | |
"test", | |
|mut e| { | |
let pos = e.get_position(); | |
let delta = Vec3 { | |
x: 10.0, | |
y: 0.0, | |
z: 5.0, | |
}; | |
e.set_position(match pos { | |
None => Vec3 { | |
x: 10.0, | |
y: 1.0, | |
z: 5.0, | |
}, | |
Some(v) => Vec3 { | |
x: v.x + delta.x, | |
y: v.y + delta.y, | |
z: v.z + delta.z, | |
}, | |
}); | |
}, | |
))); | |
let _ = em.add(Box::new(GenericEntity::new( | |
"AnotherInvalid", | |
"test", | |
|_| (), | |
))); | |
em.update(); | |
em.update(); | |
em.update(); | |
em.update(); | |
em.update(); | |
let entity = em.get(id).unwrap(); | |
let pos = entity.get_position().unwrap(); | |
assert_eq!(pos.x, 50.0); | |
assert_eq!(pos.y, 1.0); | |
assert_eq!(pos.z, 25.0); | |
} | |
#[test] | |
fn test_object_delete() -> Result<(), String> { | |
let mut em = EntityManager::new(); | |
let _ = em.add(Box::new(GenericEntity::new("Invalid", "test", |_| ()))); | |
let id = em.add(Box::new(GenericEntity::new("TestDelete", "test", |_| ()))); | |
let _ = em.add(Box::new(GenericEntity::new( | |
"AnotherInvalid", | |
"test", | |
|_| (), | |
))); | |
assert_eq!(em.count(), 3); | |
em.remove(id); | |
assert_eq!(em.count(), 2); | |
match em.get(id) { | |
Some(_) => Err(String::from("this object should not exist now")), | |
None => Ok(()), | |
} | |
} | |
} | |
fn main() { | |
println!("Hello, world!"); | |
let mut em = EntityManager::new(); | |
let mut de = GenericEntity::new("Test1", "test", |_| ()); | |
de.get_location_component_mut() | |
.unwrap() | |
.update_mesh(MeshObject {}); | |
println!("{:?}", em); | |
println!("{:?}", de); | |
let id = em.add(Box::new(de)); | |
println!("{}", id); | |
println!("{:?}", em); | |
let mut de2 = GenericEntity::new("Test2", "test", |_| ()); | |
let id = em.add(Box::new(de2)); | |
em.get_mut(1).unwrap().set_position(Vec3 { | |
x: 10.0, | |
y: 1.0, | |
z: 10.0, | |
}); | |
println!("{}", id); | |
println!("{:?}", em); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment