feat: initial recorces implimentation, basic features

This commit is contained in:
2024-01-06 01:59:33 +01:00
commit 9a6965fbb9
7 changed files with 4177 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
/target
/.vscode
.env
/.idea
Generated
+3488
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "bevy_controls"
version = "0.1.0"
edition = "2021"
[features]
default = []
serialize = ["bevy_input/serialize", "serde"]
reflect = ["dep:bevy_reflect"]
inspector-egui = ["reflect", "dep:bevy-inspector-egui"]
[dependencies]
bevy-inspector-egui = { version = "0.22.1", optional = true }
bevy_derive = "0.12.1"
bevy_ecs = "0.12.1"
bevy_input = "0.12.1"
bevy_reflect = { version = "0.12.1", optional = true }
bevy_utils = "0.12.1"
log = "0.4.20"
serde = { version = "1.0.194", optional = true }
strum = "0.25.0"
strum_macros = "0.25.3"
+2
View File
@@ -0,0 +1,2 @@
[toolchain]
channel = '1.72.0'
+4
View File
@@ -0,0 +1,4 @@
pub mod resource;
mod util;
pub use util::*;
+583
View File
@@ -0,0 +1,583 @@
use std::error;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::system::Resource;
use bevy_input::{keyboard::KeyCode, mouse::MouseButton};
#[cfg(feature = "inspector-egui")]
use bevy_inspector_egui::prelude::*;
#[cfg(all(feature = "reflect", feature = "serialize"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use bevy_utils::HashMap;
use log::warn;
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
use std::mem::discriminant;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[cfg(feature = "reflect")]
use {bevy_ecs::prelude::ReflectResource, bevy_reflect::Reflect};
use crate::{common_traits_conditions, hashmap};
/// Validate that all enum variants are in the hash map
pub fn validate_hash_map<K, V>(hash_map: &HashMap<K, V>) -> bool
where
K: Eq + std::hash::Hash + Copy + IntoEnumIterator,
K::Iterator: Iterator<Item = K>,
{
let all_keys = K::iter().collect::<Vec<_>>();
if hash_map.len() != all_keys.len() {
return false;
}
for key in all_keys {
if !hash_map.contains_key(&key) {
return false;
}
}
true
}
common_traits_conditions! {
/// Struct that contains user's inputs corresponding to actions
#[derive(Debug, PartialEq, Clone, Deref, DerefMut)]
pub struct Inputs(HashMap<Action, InputValue>);
/// Resource that contains current user inputs
#[derive(Debug, PartialEq, Clone)]
pub struct PlayerInputs {
current: Inputs,
previous: Inputs,
}
}
impl Default for PlayerInputs {
fn default() -> Self {
PlayerInputs {
current: Inputs(
Action::iter()
.map(|action| (action, InputValue::Empty))
.collect(),
),
previous: Inputs(
Action::iter()
.map(|action| (action, InputValue::Empty))
.collect(),
),
}
}
}
impl PlayerInputs {
/// Returns current input value for action
///
/// in any case faster that `get_many` method
pub fn get(&self, action: Action) -> InputValue {
// SAFETY: action is always valid
// because we iterate over all actions in `default` method
*self.current.get(&action).unwrap()
}
/// Returns current input values for actions
pub fn get_many(&self, actions: Vec<Action>) -> Vec<InputValue> {
// SAFETY: action is always valid
// because we iterate over all actions in `default` method
actions
.iter()
.map(|action| *self.current.get(action).unwrap())
.collect()
}
/// Returns `true` if current `InputValue` has become `InputValue::Boolean(true)` in this frame
/// If input has not same type as `InputValue` on previous frame call `panic`
pub fn just_pressed(&self, action: Action) -> bool {
// SAFETY: action is always valid
// because we iterate over all actions in `default` method
if let InputValue::Boolean(current) = *self.current.get(&action).unwrap() {
if let InputValue::Boolean(previous) = *self.previous.get(&action).unwrap() {
return current && !previous;
}
panic!("Previous input is not boolean");
}
panic!("This input is not boolean")
}
/// Returns `true` if current `InputValue` has become `InputValue::Boolean(true)` in this frame
/// If input has not same type as `InputValue` on previous frame return `Error()`
pub fn get_just_pressed(&self, action: Action) -> Result<bool, Box<dyn error::Error>> {
// SAFETY: action is always valid
// because we iterate over all actions in `default` method
if let InputValue::Boolean(current) = *self.current.get(&action).unwrap() {
if let InputValue::Boolean(previous) = *self.previous.get(&action).unwrap() {
return Ok(current && !previous);
}
return Err("Previous input is not boolean".into());
}
Err("This input is not boolean".into())
}
/// Set input value to new
pub fn forced_set(&mut self, action: Action, value: impl Into<InputValue>) {
// SAFETY: action is always valid
// because we iterate over all actions in `default` method
let current_input = self.current.get_mut(&action).unwrap();
*self.previous.get_mut(&action).unwrap() = *current_input;
*current_input = value.into();
}
/// Set input value to new
/// if it is not the same InputValue type return `Error()`
pub fn set(
&mut self,
action: Action,
value: impl Into<InputValue>,
) -> Result<(), Box<dyn error::Error>> {
// SAFETY: action is always valid
// because we iterate over all actions in `default` method
let current_input = self.current.get_mut(&action).unwrap();
let previos_input = self.previous.get_mut(&action).unwrap();
// check if enum type is the same ignoring value
if discriminant(current_input) == discriminant(previos_input) {
*previos_input = *current_input;
*current_input = value.into();
Ok(())
} else {
Err("Input type is not the same".into())
}
}
/// Update current inputs
pub fn forced_update(&mut self, inputs: Inputs) {
*self.previous = self.current.clone().0;
*self.current = inputs.0;
}
/// Update current inputs
/// if any input is not the same InputValue type return `Error()`
/// ! remember that it work safely but slow
pub fn update(&mut self, inputs: Inputs) -> Result<(), Box<dyn error::Error>> {
// SAFETY: action is always valid
// because we iterate over all actions in `default` method
for (action, value) in inputs.0.iter() {
let current_input = self.current.get_mut(action).unwrap();
let previos_input = self.previous.get_mut(action).unwrap();
// check if enum type is not the same ignoring value
if discriminant(current_input) != discriminant(previos_input) {
return Err("Input type is not the same".into());
}
*previos_input = *current_input;
*current_input = *value;
}
Ok(())
}
}
common_traits_conditions! {
// TODO: it must be a trait
#[derive(Debug, PartialEq, Clone)]
pub enum GameState {
LevelEditor,
InGame,
}
#[derive(Debug, PartialEq, Clone)]
pub enum InputType {
Keyboard(KeyCode),
Mouse(MouseInput),
// TODO: Gamepad(GamepadButtonType),
// TODO: Touch screen
}
#[derive(Debug, PartialEq, Clone)]
pub enum MouseInput {
Button(MouseButton),
Axis(AxisName),
Wheel(AxisName)
// TODO: CursorPosition
// TODO: TouchPad inputs
}
/// Name of axis
#[derive(Debug, PartialEq, Clone)]
pub enum AxisName {
Horizontal,
Vertical,
}
/// Represents a binding that can be changed by the player
#[derive(Debug, PartialEq, Clone)]
pub enum OptionsMode {
/// Represents a binding that cannot be changed as it is crucial to the game logic
Immutable,
/// Represents a binding that the player can customize
Customizable,
}
/// Represents the manner in which input is registered
#[derive(Debug, PartialEq, Clone)]
pub enum ActivationMode {
/// Action is triggered when the button is pressed
Hold,
/// Action is triggered when the button is pressed
/// and cannot be triggered again until the button is released
/// you don't need to release all buttons in chord
/// you can release only last pressed button
Tap,
}
pub struct ActivationOptions {
/// Manner in which input is registered
pub mode: ActivationMode,
/// Possibility to change this activation mode
pub option: OptionsMode,
/// Delay between activation
pub delay: f32,
}
}
impl Default for ActivationOptions {
fn default() -> Self {
Self {
mode: ActivationMode::Tap, // Because it is more safe
option: OptionsMode::Immutable,
delay: 0.05, // 20 times per second; 14 - world record?
}
}
}
impl ActivationOptions {
pub fn new(mode: ActivationMode, option: OptionsMode) -> Self {
Self {
mode,
option,
..Default::default()
}
}
pub fn with_delay(mut self, delay: f32) -> Self {
self.delay = delay;
self
}
}
common_traits_conditions! {
/// Represents a combination of buttons that triggers an action
#[derive(Debug, PartialEq, Clone)]
pub enum ButtonCombination {
/// Single button press
Single(InputType),
/// Chord is a combination of buttons that must be pressed at the same time
Chord(Vec<InputType>),
}
#[derive(Debug, PartialEq, Clone)]
pub enum BindingCondition {
/// During specific game state
InGameState(GameState),
/// Binding is active only if player is in pause menu
DuringPauseMenu(bool),
/// Binding is active only if player types text
ListeningForText(bool),
}
// TODO: it is just plug, it must be a trait
/// Actions that can be performed by player
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, EnumIter)]
pub enum Action {
/// Move forward
LeverEditorForward,
/// Move backward
LevelEditorBackward,
/// Move left
LevelEditorLeft,
/// Move right
LevelEditorRight,
LevelEditorFly,
}
/// Binding is a combination of buttons that triggers action
#[derive(Debug, PartialEq, Clone)]
pub struct Binding {
pub input: ButtonCombination,
pub conditions: Vec<BindingCondition>,
}
}
impl From<ButtonCombination> for Binding {
fn from(input: ButtonCombination) -> Self {
Binding {
input,
conditions: Vec::new(),
}
}
}
impl Binding {
pub fn new(input: ButtonCombination) -> Self {
Self {
input,
conditions: Vec::new(),
}
}
pub fn with_condition(mut self, condition: BindingCondition) -> Self {
self.conditions.push(condition);
self
}
}
common_traits_conditions! {
/// Input value that can be used in game logic
#[derive(Default, Clone, Copy, PartialEq, Debug)]
pub enum InputValue {
#[default]
Empty,
Boolean(bool),
Float(f32),
}
}
impl From<bool> for InputValue {
fn from(value: bool) -> Self {
InputValue::Boolean(value)
}
}
impl From<f32> for InputValue {
fn from(value: f32) -> Self {
InputValue::Float(value)
}
}
impl From<InputValue> for bool {
fn from(value: InputValue) -> Self {
match value {
InputValue::Boolean(value) => value,
_ => panic!("InputValue is not boolean"),
}
}
}
impl From<InputValue> for f32 {
fn from(value: InputValue) -> Self {
match value {
InputValue::Float(value) => value,
_ => panic!("InputValue is not float"),
}
}
}
impl InputValue {
pub fn is_empty(&self) -> bool {
matches!(self, InputValue::Empty)
}
pub fn is_boolean(&self) -> bool {
matches!(self, InputValue::Boolean(_))
}
pub fn is_float(&self) -> bool {
matches!(self, InputValue::Float(_))
}
pub fn as_boolean(&self) -> bool {
match self {
InputValue::Boolean(value) => *value,
InputValue::Float(value) => *value > 0.0,
InputValue::Empty => false,
}
}
}
common_traits_conditions! {
pub struct Bindings {
/// List of bindings for action
list: Vec<Binding>,
/// Possibility to change this binding
options: OptionsMode,
}
}
impl Default for Bindings {
fn default() -> Self {
Self {
list: Vec::new(),
options: OptionsMode::Immutable,
}
}
}
impl Bindings {
pub fn new(bindings: Vec<Binding>, options: OptionsMode) -> Self {
Self {
list: bindings,
options,
}
}
pub fn push(&mut self, binding: Binding) {
if self.options == OptionsMode::Immutable {
warn!("You try to push binding to immutable bindings");
return;
}
self.list.push(binding);
}
pub fn clear(&mut self) {
if self.options == OptionsMode::Immutable {
warn!("You try to clear immutable bindings");
return;
}
self.list.clear();
}
pub fn retain<F>(&mut self, f: F)
where
F: FnMut(&Binding) -> bool,
{
if self.options == OptionsMode::Immutable {
warn!("You try to retain immutable bindings");
return;
}
self.list.retain(f);
}
pub fn iter(&self) -> impl Iterator<Item = &Binding> {
self.list.iter()
}
pub fn iter_mut(
&mut self,
) -> Result<impl Iterator<Item = &mut Binding>, Box<dyn error::Error>> {
if self.options == OptionsMode::Immutable {
warn!("You try to iterate over immutable bindings");
return Err("You try to iterate over immutable bindings".into());
}
Ok(self.list.iter_mut())
}
pub fn is_customizable(&self) -> bool {
self.options == OptionsMode::Customizable
}
pub fn is_immutable(&self) -> bool {
self.options == OptionsMode::Immutable
}
}
common_traits_conditions! {
/// Contains all bindings for action
/// and different activation options
#[derive(Default)]
pub struct BindingConfig {
pub bindings: Bindings,
#[cfg_attr(feature = "serialize", serde(skip))]
pub activation: ActivationOptions,
}
}
impl BindingConfig {
pub fn new(bindings: Bindings, activation: ActivationOptions) -> Self {
Self {
bindings,
activation,
}
}
}
common_traits_conditions! {
/// Contains all bindings for actions
#[derive(Resource)]
#[cfg_attr(feature = "reflect", reflect(Resource))]
// TODO: Add delay for input
pub struct Controls(HashMap<Action, BindingConfig>); // TODO: change HashMap to Vec for faster iteration
}
impl Default for Controls {
fn default() -> Self {
// TODO: default must create empty controls or fill empty / default BindingConfig on any actions
let controls = Self(hashmap! {
Action::LeverEditorForward => BindingConfig::new(
Bindings::new(
vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::W))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))],
OptionsMode::Customizable,
),
ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable),
),
Action::LevelEditorBackward => BindingConfig::new(
Bindings::new(
vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::S))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))],
OptionsMode::Customizable,
),
ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable),
),
Action::LevelEditorLeft => BindingConfig::new(
Bindings::new(
vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::A))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))],
OptionsMode::Customizable,
),
ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable),
),
Action::LevelEditorRight => BindingConfig::new(
Bindings::new(
vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::D))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))],
OptionsMode::Customizable,
),
ActivationOptions::new(ActivationMode::Hold, OptionsMode::Immutable),
),
Action::LevelEditorFly => BindingConfig::new(
Bindings::new(
vec![Binding::from(ButtonCombination::Single(InputType::Keyboard(KeyCode::Space))).with_condition(BindingCondition::InGameState(GameState::LevelEditor))],
OptionsMode::Customizable,
),
ActivationOptions::new(ActivationMode::Hold, OptionsMode::Customizable),
)
});
// If you see this error, you may add new action in menu_actions
// or make sure that you have only one MenuAction with the same name in the MenuActions
assert!(validate_hash_map(&controls.0));
controls
}
}
impl Controls {
/// Returns bindings for action
pub fn get(&self, action: Action) -> Option<&BindingConfig> {
self.0.get(&action)
}
/// Push new binding for action
pub fn push(&mut self, action: Action, binding: Binding) {
// TODO: warning if config is not exist yet
self.0
.entry(action)
.or_insert_with(BindingConfig::default)
.bindings
.push(binding);
}
/// Remove all bindings for action
pub fn remove(&mut self, action: Action) {
// TODO: warning if config is not exist yet
self.0
.entry(action)
.or_insert_with(BindingConfig::default)
.bindings
.clear();
}
pub fn remove_binding(&mut self, action: Action, binding: Binding) {
if let Some(config) = self.0.get_mut(&action) {
config.bindings.retain(|b| *b == binding);
}
}
pub fn iter(&self) -> impl Iterator<Item = (&Action, &BindingConfig)> {
self.0.iter()
}
}
+74
View File
@@ -0,0 +1,74 @@
/// Creates a [`HashMap`](bevy_utils::HashMap) from a list of key-value pairs.
///
/// Example:
/// ```ignore
/// let map = hashmap! {
/// "key1" => "value1",
/// "key2" => "value2"
/// };
#[macro_export]
macro_rules! hashmap {
($( $key: expr => $val: expr ),*) => {{
let mut map = HashMap::new();
$(
map.insert($key, $val);
)*
map
}};
}
/// Applies common traits conditionally to items.
///
/// This macro is designed to reduce repetition and maintenance overhead by
/// automatically deriving specified traits for each item passed to it based
/// on the feature flags. It's intended for use with almost all types in this module.
///
/// # Features:
/// - `serialize`: Derives `Serialize` and `Deserialize` traits and applies
/// reflection for serialization if the `serialize` feature is enabled.
/// - `reflect`: Derives the `Reflect` trait if the `reflect` feature is enabled.
/// - `inspector-egui`: Derives `InspectorOptions` trait and applies reflection
/// for the inspector if the `inspector-egui` feature is enabled.
///
/// # Usage
/// To use this macro, wrap any item(s) you want to apply the common traits to.
/// Each item can be a struct or enum that you want to derive traits for based
/// on the feature flags.
///
/// # Example:
/// ```ignore
/// common_traits_conditions! {
/// struct MyStruct {
/// // fields
/// }
///
/// enum MyEnum {
/// // variants
/// }
/// }
/// ```
#[macro_export]
macro_rules! common_traits_conditions {
($($item:item)*) => {
$(
#[cfg_attr(
feature = "serialize",
derive(Serialize, Deserialize),
)]
#[cfg_attr(
feature = "reflect",
derive(Reflect),
)]
#[cfg_attr(
all(feature = "reflect", feature = "serialize"),
reflect(Serialize, Deserialize),
)]
#[cfg_attr(
feature = "inspector-egui",
derive(InspectorOptions),
reflect(InspectorOptions),
)]
$item
)*
};
}