From 5ba65113e67b321427443bb708cc1f861ca8af15 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Sat, 28 Dec 2024 16:59:05 -0500 Subject: [PATCH 1/4] feat: more realistic --- src/ball.rs | 104 ++++++++++---------- src/player.rs | 266 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 237 insertions(+), 133 deletions(-) diff --git a/src/ball.rs b/src/ball.rs index b0ca3e1..45670e9 100644 --- a/src/ball.rs +++ b/src/ball.rs @@ -15,10 +15,11 @@ use bevy_rapier2d::prelude::*; /// Physical properties of the ball that define its behavior const BALL_SIZE: f32 = 0.3; // Diameter in world units -const BALL_SPEED: f32 = 10.0; // Constant speed the ball should maintain -const SPEED_TOLERANCE: f32 = 0.5; // Allowed speed variation before correction -const RESTITUTION: f32 = 1.5; // Bounciness (>1 means gaining energy) -const BALL_MASS: f32 = 0.1; // Mass affects collision response +const MIN_VELOCITY: f32 = 7.0; // Minimum speed for the ball +const MAX_VELOCITY: f32 = 32.0; // Maximum speed for the ball +const SPEED_TOLERANCE: f32 = 3.0; // Allowed speed variation before correction +const RESTITUTION: f32 = 0.9; // Bounciness (>1 means gaining energy) +const BALL_MASS: f32 = 0.0027; // Mass affects collision response /// Marker component to identify ball entities. /// Used for querying and managing ball-specific behavior. @@ -38,41 +39,42 @@ pub fn create_ball( materials: &mut ResMut>, served_by_p1: bool, ) { - // Determine initial direction based on server let direction = if served_by_p1 { 1 } else { -1 }; + let initial_velocity = Vec2::new(MIN_VELOCITY * direction as f32, 0.0); - commands.spawn(( - // Identification and visual components - Ball, // Marker component - Mesh2d(meshes.add(Circle::new(BALL_SIZE / 2.0))), // Visual shape - MeshMaterial2d(materials.add(ColorMaterial::from(Color::WHITE))), // Color - Transform::from_xyz(0.0, 0.0, 0.0), // Start at center - // Physics body configuration - RigidBody::Dynamic, // Moves based on physics - Collider::ball(BALL_SIZE / 2.0), // Circular collision shape - // Initial velocity (horizontal only) - Velocity::linear(Vec2::new(BALL_SPEED * direction as f32, 0.0)), - // Collision response properties - Restitution { + commands + .spawn(Ball) + // Visual components + .insert(Mesh2d(meshes.add(Circle::new(BALL_SIZE / 2.0)))) + .insert(MeshMaterial2d( + materials.add(ColorMaterial::from(Color::WHITE)), + )) + .insert(Transform::from_xyz(0.0, 0.0, 0.0)) + // Physics body setup + .insert(RigidBody::Dynamic) + .insert(Collider::ball(BALL_SIZE / 2.0)) + .insert(Velocity::linear(initial_velocity)) + // Collision properties + .insert(Restitution { coefficient: RESTITUTION, - combine_rule: CoefficientCombineRule::Max, // Use highest restitution in collisions - }, - Friction { - coefficient: 0.0, // Frictionless - combine_rule: CoefficientCombineRule::Min, // Use lowest friction in collisions - }, - // Physics behavior modifiers - Damping { - linear_damping: 0.0, // No speed loss over time - angular_damping: 0.0, // No rotation slowdown - }, - GravityScale(0.0), // Ignore gravity - // Collision detection settings - Ccd::enabled(), // Continuous collision detection - ActiveCollisionTypes::all(), // Detect all collision types - ActiveEvents::COLLISION_EVENTS, // Generate collision events - AdditionalMassProperties::Mass(BALL_MASS), // Set specific mass - )); + combine_rule: CoefficientCombineRule::Max, + }) + .insert(Friction { + coefficient: 0.0, + combine_rule: CoefficientCombineRule::Min, + }) + // Physics modifiers + .insert(Damping { + linear_damping: 0.0, + angular_damping: 0.0, + }) + .insert(GravityScale(0.0)) + // Collision detection + .insert(Ccd::enabled()) + .insert(Sleeping::disabled()) + .insert(ActiveCollisionTypes::all()) + .insert(ActiveEvents::COLLISION_EVENTS) + .insert(AdditionalMassProperties::Mass(BALL_MASS)); } /// System to clean up the ball when exiting the Playing state. @@ -83,24 +85,22 @@ fn cleanup_ball(mut commands: Commands, ball_query: Query>) { } } -/// System that maintains constant ball speed regardless of collisions. -/// -/// This is necessary because: -/// 1. Collisions can change the ball's speed -/// 2. We want the game to maintain a consistent pace -/// 3. Numerical errors can accumulate over time -/// -/// The system checks the current speed against BALL_SPEED and -/// corrects it if it deviates beyond SPEED_TOLERANCE. fn maintain_ball_velocity(mut query: Query<&mut Velocity, With>) { for mut velocity in query.iter_mut() { let current_velocity = velocity.linvel; let current_speed = current_velocity.length(); - // Correct speed if it's outside tolerance range - if (current_speed - BALL_SPEED).abs() > SPEED_TOLERANCE { - // Maintain direction but normalize speed - velocity.linvel = current_velocity.normalize() * BALL_SPEED; + // Check absolute magnitude and adjust if needed + if current_speed != 0.0 { // Prevent division by zero + let new_speed = if current_speed.abs() < MIN_VELOCITY { + MIN_VELOCITY + } else if current_speed.abs() > MAX_VELOCITY { + MAX_VELOCITY + } else { + current_speed + }; + + velocity.linvel = current_velocity.normalize() * new_speed; } } } @@ -113,10 +113,6 @@ impl Plugin for BallPlugin { app // Clean up ball when leaving Playing state .add_systems(OnExit(GameState::Playing), cleanup_ball) - // Maintain ball velocity during gameplay - .add_systems( - Update, - maintain_ball_velocity.run_if(in_state(GameState::Playing)), - ); + .add_systems(Update, maintain_ball_velocity); } } diff --git a/src/player.rs b/src/player.rs index bb52310..b504c13 100644 --- a/src/player.rs +++ b/src/player.rs @@ -12,21 +12,28 @@ use crate::ball::Ball; use crate::GameState; use bevy::app::{App, Plugin, Startup, Update}; use bevy::prelude::*; +use bevy::render::mesh::Indices; +use bevy::render::render_asset::RenderAssetUsages; +use bevy::render::render_resource::PrimitiveTopology; use bevy_rapier2d::prelude::*; /// Constants defining the player paddles' physical and gameplay properties -const PADDLE_WIDTH: f32 = 0.5; // Width in world units -const PADDLE_HEIGHT: f32 = 2.0; // Height in world units -const PADDLE_SPEED: f32 = 5.0; // Movement speed in units per second +const PADDLE_SPEED: f32 = 20.0; // Movement speed in units per second const MAX_BOUNCE_ANGLE: f32 = 1.0; // Maximum angle ball can bounce (radians) const LEFT_PADDLE_X: f32 = -7.65; // X position of left paddle const RIGHT_PADDLE_X: f32 = 7.65; // X position of right paddle +// Define scoop paddle shape constants +const PADDLE_WIDTH: f32 = 0.5; // Base width of the paddle +const PADDLE_HEIGHT: f32 = 2.0; // Height of the paddle +const PADDLE_DEPTH: f32 = 0.3; // How deep the curve goes +const SEGMENTS: usize = 100; // Number of segments to create the curve + /// Update rate for AI decisions to simulate human-like reaction time const AI_UPDATE_RATE: f32 = 0.3; // Updates 3.33 times per second /// Component that identifies which player a paddle belongs to -#[derive(Component)] +#[derive(Component, Clone)] pub enum Player { P1, // Human player (left paddle) P2, // AI player (right paddle) @@ -40,6 +47,14 @@ struct AiPaddle { move_down: bool, // Simulates S key press } +/// Component to track paddle punch state +#[derive(Component)] +struct PunchState { + timer: Timer, + is_punching: bool, + rest_x: f32, // The x position to return to +} + impl Default for AiPaddle { fn default() -> Self { Self { @@ -50,6 +65,46 @@ impl Default for AiPaddle { } } +impl Default for PunchState { + fn default() -> Self { + Self { + timer: Timer::from_seconds(0.05, TimerMode::Once), // 50ms punch duration + is_punching: false, + rest_x: 0.0, // Will be set on spawn + } + } +} + +/// Helper function to generate vertices for a segment of the scoop paddle shape +/// Creates a paddle with a flat back and curved front +/// +/// # Arguments +/// * `index` - Index of the current segment being generated +/// * `total_segments` - Total number of segments in the paddle +/// +/// # Returns +/// * Vector of Vec2 points defining a convex segment of the paddle +fn generate_segment_vertices(index: usize, total_segments: usize) -> Vec { + let segment_height = PADDLE_HEIGHT / (total_segments as f32); + let y_start = -PADDLE_HEIGHT / 2.0 + (index as f32 * segment_height); + let y_end = y_start + segment_height; + + // Calculate x position for the curved front using a parabolic curve + // The curve bulges outward from the flat back + let curve = |y: f32| -> f32 { + let normalized_y = (y + PADDLE_HEIGHT / 2.0) / PADDLE_HEIGHT; + // Start from left side and curve outward + PADDLE_DEPTH * (4.0 * normalized_y * (1.0 - normalized_y)) + }; + + vec![ + Vec2::new(0.0, y_start), // Back left (flat) + Vec2::new(curve(y_start), y_start), // Front curved + Vec2::new(curve(y_end), y_end), // Front curved + Vec2::new(0.0, y_end), // Back right (flat) + ] +} + /// Calculates where the ball will intersect with a paddle's x-position /// /// # Arguments @@ -85,7 +140,6 @@ fn ai_decision_making( mut ai_query: Query<(&Transform, &mut AiPaddle)>, ) { for (paddle_transform, mut ai) in ai_query.iter_mut() { - // Update AI decisions at fixed intervals if ai.update_timer.tick(time.delta()).just_finished() { if let Ok((ball_transform, ball_velocity)) = ball_query.get_single() { if let Some(predicted_y) = predict_intersection( @@ -93,8 +147,16 @@ fn ai_decision_making( ball_velocity.linvel, RIGHT_PADDLE_X, ) { + // Add offset to account for curved surface optimal hit point + let optimal_y = predicted_y + + (if ball_velocity.linvel.y > 0.0 { + -PADDLE_HEIGHT / 4.0 + } else { + PADDLE_HEIGHT / 4.0 + }); + // Calculate difference between current and target position - let diff = predicted_y - paddle_transform.translation.y; + let diff = optimal_y - paddle_transform.translation.y; // Reset movement flags ai.move_up = false; @@ -113,13 +175,6 @@ fn ai_decision_making( } /// Unified system that handles both human and AI paddle movement -/// -/// For human players: -/// - W/Up Arrow: Move up -/// - S/Down Arrow: Move down -/// -/// For AI: -/// - Uses simulated input from AI decision making system fn paddle_movement( input: Res>, time: Res