feat: Basic player implementation

This commit is contained in:
Alexey 2026-04-10 13:35:44 +03:00
commit e4b1475c48
9 changed files with 830 additions and 54 deletions

62
src/input.rs Normal file
View file

@ -0,0 +1,62 @@
//! Input module
use bevy::prelude::*;
use leafwing_input_manager::prelude::*;
/// This trait is used to provide uniform way of getting predefined [InputMap]
pub trait DefaultInputMap: Actionlike {
/// Get default input map for the Actionlike
fn input_map() -> InputMap<Self>;
}
/// Input used to control player character
#[derive(Actionlike, Debug, PartialEq, Eq, Hash, Clone, Copy, Reflect)]
#[reflect(Debug, PartialEq, Hash, Clone)]
pub enum PlayerInput {
/// Move character
#[actionlike(Axis)] Move,
/// Dodge with light weapon or block with heavy weapon
DodgeBlock,
/// Perform light attack
LightAttack,
/// Perform heavy attack
HeavyAttack,
}
impl DefaultInputMap for PlayerInput {
fn input_map() -> InputMap<Self> {
InputMap::default()
.with_axis(Self::Move, VirtualAxis::ad())
.with_axis(Self::Move, GamepadAxis::LeftStickX)
.with(Self::DodgeBlock, KeyCode::ShiftLeft)
.with(Self::DodgeBlock, GamepadButton::South)
.with(Self::LightAttack, MouseButton::Left)
.with(Self::HeavyAttack, GamepadButton::North)
.with(Self::LightAttack, MouseButton::Right)
.with(Self::HeavyAttack, GamepadButton::East)
}
}
/// Input used to debug this prototype
#[derive(Actionlike, Debug, PartialEq, Eq, Hash, Clone, Copy, Reflect)]
#[reflect(Debug, PartialEq, Hash, Clone)]
pub enum DebugInput {
/// Reset game state
Reset,
/// Spawn enemy in predefined position
SpawnEnemy,
}
impl DefaultInputMap for DebugInput {
fn input_map() -> InputMap<Self> {
InputMap::default()
.with(Self::Reset, KeyCode::KeyR)
.with(Self::Reset, GamepadButton::Select)
.with(Self::SpawnEnemy, KeyCode::KeyT)
.with(Self::SpawnEnemy, GamepadButton::LeftTrigger)
}
}

View file

@ -6,3 +6,21 @@
mod tests;
pub mod graph;
pub mod input;
pub mod player;
pub mod plugin;
use bevy::prelude::*;
use crate::player::Player;
const PIXELS_PER_METER: f32 = 16.;
/// Returns pixel measurement for given length in meters
#[inline(always)] pub const fn meters(length: f32) -> f32 { PIXELS_PER_METER * length }
// TODO: Replace with proper setting up
/// Temporary function to setup world
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Player::bundle(&asset_server, Vec2::ZERO));
}

View file

@ -1,54 +1,19 @@
use bevy::prelude::*;
#[derive(Component)]
struct Person;
#[derive(Component)]
struct Name(String);
#[derive(Resource)]
struct GreetTimer(Timer);
#[derive(Resource)]
struct UpdateTimer(Timer);
pub struct HelloPlugin;
impl Plugin for HelloPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(GreetTimer(Timer::from_seconds(2.0, TimerMode::Repeating)));
app.insert_resource(UpdateTimer(Timer::from_seconds(10.0, TimerMode::Once)));
app.add_systems(Startup, add_people);
app.add_systems(Update, (update_people, greet_people).chain());
}
}
fn add_people(mut commands: Commands) {
commands.spawn((Person, Name("Alkesey Mirnekov".to_string())));
commands.spawn((Person, Name("Alkesey Mirnekov 2".to_string())));
commands.spawn((Person, Name("Alkesey Mirnekov 3".to_string())));
}
fn greet_people(time: Res<Time>, mut timer: ResMut<GreetTimer>, query: Query<&Name, With<Person>>) {
if timer.0.tick(time.delta()).just_finished() {
for name in &query {
println!("hello {}!", name.0);
}
}
}
fn update_people(time: Res<Time>, mut timer: ResMut<UpdateTimer>, mut query: Query<&mut Name, With<Person>>) {
if timer.0.tick(time.delta()).just_finished() {
for mut name in &mut query {
name.0 = format!("{} II", name.0);
}
}
}
use bevy::{
prelude::*,
remote::{
RemotePlugin,
http::RemoteHttpPlugin,
},
};
use bevy_combat_proto::plugin::GamePlugin;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(HelloPlugin)
.add_plugins((
DefaultPlugins.set(ImagePlugin::default_nearest()),
GamePlugin,
RemotePlugin::default(),
RemoteHttpPlugin::default(),
))
.run();
}

76
src/player/mod.rs Normal file
View file

@ -0,0 +1,76 @@
//! Player module
use bevy::{camera::ScalingMode, prelude::*};
use bevy_rapier2d::prelude::*;
use leafwing_input_manager::prelude::*;
use crate::{
meters,
input::{
DefaultInputMap,
PlayerInput,
}
};
pub mod systems;
/// Player component
#[derive(Component, Clone, Copy, PartialEq, Debug, Reflect)]
#[reflect(Component, Clone, PartialEq, Debug, Default)]
#[require(Transform, InputMap<PlayerInput>)]
pub struct Player {
/// Movement speed in pixels/s
pub speed: f32,
}
impl Default for Player {
fn default() -> Self {
Self {
speed: meters(1.),
}
}
}
impl Player {
/// Returns player bundle with everything needed
pub fn bundle(asset_server: &Res<AssetServer>, position: Vec2) -> impl Bundle {
let image = asset_server.load("sprites/player/placeholder.png");
(
// Basic
Name::new("Player"),
Player::default(),
// Visible
Sprite::from_image(image),
Transform::from_xyz(position.x, position.y, 1.),
// Input
PlayerInput::input_map(),
// Collision
RigidBody::KinematicPositionBased,
KinematicCharacterController::default(),
ActiveCollisionTypes::default() | ActiveCollisionTypes::KINEMATIC_STATIC,
Collider::cuboid(meters(0.3), meters(0.9)),
ActiveEvents::COLLISION_EVENTS,
Sleeping::disabled(),
Children::spawn((
Spawn((
Name::new("Player camera"),
Camera2d,
Camera {
clear_color: ClearColorConfig::Custom(Color::hsl(0.02, 0.67, 0.65)),
..default()
},
Projection::Orthographic(OrthographicProjection {
scaling_mode: ScalingMode::FixedVertical {
viewport_height: meters(24.),
},
..OrthographicProjection::default_2d()
}),
)),
)),
)
}
}

25
src/player/systems.rs Normal file
View file

@ -0,0 +1,25 @@
//! Player systems
use super::*;
/// Do something based on [PlayerInput]
pub fn handle_input(
time: Res<Time>,
player_query: Query<(
&Player,
&ActionState<PlayerInput>,
&mut KinematicCharacterController,
&mut Sprite,
)>,
) {
for (player, action_state, mut controller, mut sprite) in player_query {
let direction = action_state.clamped_value(&PlayerInput::Move);
controller.translation = Some(vec2(direction * player.speed * time.delta_secs(), 0.));
if direction != 0. {
sprite.flip_x = direction < 0.;
}
}
}

25
src/plugin.rs Normal file
View file

@ -0,0 +1,25 @@
//! Plugin module where everything is connected
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
use leafwing_input_manager::prelude::*;
use crate::*;
/// Plugin that connects everything needed for this prototype
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
RapierDebugRenderPlugin::default(),
RapierPhysicsPlugin::<()>::default()
.with_length_unit(meters(1.)),
InputManagerPlugin::<input::PlayerInput>::default(),
InputManagerPlugin::<input::DebugInput>::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, player::systems::handle_input);
}
}