Skip to content

Commit

Permalink
Character Controller and basic stuff (#46)
Browse files Browse the repository at this point in the history
Issue:
==============
Closes #6

What was done:
==============
* Added camera, animation and game related modules.
* Makes the camera follow the player.
* Added a dragon spritesheet for the player.
* Added input management for player movement via mouse.
* Added a placeholder background image.
* Fixed some noisy WGPU console warnings.
* Added build optimizations for third party crates.
* Fixed a few minor issues.
* Added a few convenience functions.
* Updated all dependencies in Cargo.lock.
  • Loading branch information
mnmaita authored Nov 12, 2023
1 parent 9e9c385 commit ccde544
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 16 deletions.
14 changes: 2 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ bevy = { version = "0.12.0", default-features = false, features = [
"vorbis",
"x11",
] }

[profile.dev.package."*"]
opt-level = 3
Binary file added assets/textures/background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/textures/dragon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions src/animation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use bevy::prelude::*;

pub struct AnimationPlugin;

impl Plugin for AnimationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, animate_sprite);
}
}

#[derive(Component)]
pub struct AnimationIndices {
first: usize,
last: usize,
}

impl AnimationIndices {
pub fn new(first: usize, last: usize) -> Self {
Self { first, last }
}
}

#[derive(Component, Deref, DerefMut)]
pub struct AnimationTimer(Timer);

impl AnimationTimer {
pub fn from_seconds(secs: f32) -> Self {
Self(Timer::from_seconds(secs, TimerMode::Repeating))
}
}

fn animate_sprite(
time: Res<Time>,
mut query: Query<(
&AnimationIndices,
&mut AnimationTimer,
&mut TextureAtlasSprite,
)>,
) {
for (indices, mut timer, mut sprite) in &mut query {
if timer.tick(time.delta()).just_finished() {
sprite.index = if sprite.index == indices.last {
indices.first
} else {
sprite.index + 1
};
}
}
}
28 changes: 28 additions & 0 deletions src/camera.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use bevy::prelude::*;

use crate::game::Player;

pub struct CameraPlugin;

impl Plugin for CameraPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, setup_camera);

app.add_systems(Update, update_camera.run_if(any_with_component::<Player>()));
}
}

fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}

fn update_camera(
mut camera_query: Query<&mut Transform, With<Camera2d>>,
player_query: Query<&Transform, (With<Player>, Without<Camera2d>)>,
) {
let player_transform = player_query.single();
let mut camera_transform = camera_query.single_mut();

camera_transform.translation.x = player_transform.translation.x;
camera_transform.translation.y = player_transform.translation.y;
}
5 changes: 5 additions & 0 deletions src/game/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod player;
mod plugin;

pub use player::Player;
pub use plugin::GamePlugin;
26 changes: 26 additions & 0 deletions src/game/player.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use bevy::prelude::*;

use crate::animation::{AnimationIndices, AnimationTimer};

#[derive(Component)]
pub struct Player;

pub(super) fn spawn_player(mut commands: Commands, asset_server: Res<AssetServer>) {
let texture = asset_server
.get_handle("textures/dragon.png")
.unwrap_or_default();
let texture_atlas = TextureAtlas::from_grid(texture, Vec2::new(191., 161.), 12, 1, None, None);
let texture_atlas_handle = asset_server.add(texture_atlas);

commands.spawn((
Player,
SpriteSheetBundle {
sprite: TextureAtlasSprite::new(0),
texture_atlas: texture_atlas_handle.clone(),
transform: Transform::from_translation(Vec2::ZERO.extend(1.)),
..default()
},
AnimationIndices::new(0, 2),
AnimationTimer::from_seconds(0.2),
));
}
23 changes: 23 additions & 0 deletions src/game/plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use bevy::prelude::*;

use crate::AppState;

use super::player::spawn_player;

pub struct GamePlugin;

impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(AppState::InGame), (draw_background, spawn_player));
}
}

fn draw_background(mut commands: Commands, asset_server: Res<AssetServer>) {
let texture = asset_server
.get_handle("textures/background.png")
.unwrap_or_default();
commands.spawn(SpriteBundle {
texture,
..default()
});
}
72 changes: 72 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use std::f32::consts::FRAC_PI_2;

use bevy::{ecs::system::SystemParam, prelude::*, window::PrimaryWindow};

use crate::{game::Player, playing};

pub struct InputPlugin;

impl Plugin for InputPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, mouse_input.run_if(playing()));
}
}

#[derive(Resource)]
struct CursorWorldPosition(Option<Vec2>);

#[derive(SystemParam)]
struct CursorWorldPositionChecker<'w, 's> {
window_query: Query<'w, 's, &'static Window, With<PrimaryWindow>>,
camera_query: Query<'w, 's, (&'static Camera, &'static GlobalTransform), With<Camera2d>>,
}

impl CursorWorldPositionChecker<'_, '_> {
pub fn cursor_world_position(&self) -> Option<Vec2> {
let window = self.window_query.single();

window.cursor_position().and_then(|cursor_position| {
let (camera, camera_transform) = self.camera_query.single();
camera.viewport_to_world_2d(camera_transform, cursor_position)
})
}
}

fn mouse_input(
mouse_input: ResMut<Input<MouseButton>>,
cursor_world_position_checker: CursorWorldPositionChecker,
mut query: Query<&mut Transform, With<Player>>,
mut gizmos: Gizmos,
) {
if mouse_input.pressed(MouseButton::Right) {
if let Some(cursor_position) = cursor_world_position_checker.cursor_world_position() {
let mut player_transform = query.single_mut();
let player_position = player_transform.translation.truncate();
let cursor_to_player_vector = cursor_position - player_position;
let cursor_distance_to_player = cursor_position.distance(player_position);
let velocity_rate = cursor_distance_to_player.min(300.) / 300.;

if cursor_to_player_vector != Vec2::ZERO {
let direction = cursor_to_player_vector.normalize();

player_transform.translation.x += direction.x * 15. * velocity_rate;
player_transform.translation.y += direction.y * 15. * velocity_rate;

if direction != Vec2::ZERO {
let angle = (direction).angle_between(Vec2::X);

if angle.is_finite() {
// FIXME: Rotate the image sprite to always face right?
// FRAC_PI_2 is subtracted to offset the 90 degree rotation from the X axis the sprite has.
player_transform.rotation = Quat::from_rotation_z(-angle - FRAC_PI_2);
}
}
}

#[cfg(debug_assertions)]
{
gizmos.line_2d(player_position, cursor_position, Color::YELLOW);
}
}
}
}
65 changes: 61 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,71 @@
use audio::AudioPlugin;
use bevy::prelude::*;
use textures::TexturesPlugin;
use animation::AnimationPlugin;
use audio::{audio_assets_loaded, AudioPlugin};
use bevy::{
prelude::*,
render::{
settings::{Backends, RenderCreation, WgpuSettings},
RenderPlugin,
},
};
use camera::CameraPlugin;
use game::GamePlugin;
use input::InputPlugin;
use textures::{texture_assets_loaded, TexturesPlugin};

mod animation;
mod audio;
mod camera;
mod game;
mod input;
mod textures;

fn main() {
let mut app = App::new();

app.add_plugins((DefaultPlugins, AudioPlugin, TexturesPlugin));
app.add_plugins((
// FIXME: Remove setting the backend explicitly to avoid noisy warnings
// when https://github.com/gfx-rs/wgpu/issues/3959 gets fixed.
DefaultPlugins.set(RenderPlugin {
render_creation: RenderCreation::Automatic(WgpuSettings {
backends: Some(Backends::DX12),
..default()
}),
}),
AnimationPlugin,
AudioPlugin,
CameraPlugin,
GamePlugin,
InputPlugin,
TexturesPlugin,
));

app.add_state::<AppState>();

app.add_systems(
Update,
handle_asset_load.run_if(assets_loaded().and_then(run_once())),
);

app.run();
}

#[derive(Clone, Debug, Default, Hash, PartialEq, Eq, States)]
pub enum AppState {
#[default]
Setup,
InGame,
}

pub fn playing() -> impl Condition<()> {
IntoSystem::into_system(in_state(AppState::InGame))
}

fn handle_asset_load(mut state: ResMut<NextState<AppState>>) {
#[cfg(debug_assertions)]
info!("Assets loaded successfully.");
state.set(AppState::InGame);
}

fn assets_loaded() -> impl Condition<()> {
texture_assets_loaded().and_then(audio_assets_loaded())
}

0 comments on commit ccde544

Please sign in to comment.