Created
August 13, 2021 10:46
-
-
Save xdcrafts/5909e364df1c1683be4b4227e9a012df to your computer and use it in GitHub Desktop.
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 ggez::graphics::{self, Color, DrawMode, DrawParam, FillOptions, Mesh, Rect}; | |
use ggez::{Context, ContextBuilder, GameError, GameResult}; | |
use ggez::mint; | |
use ggez::event::{self, EventHandler}; | |
use ggez::input::keyboard::{self, KeyCode}; | |
use rand::prelude::*; | |
use std::ops::Neg; | |
// ------------------------------------------------------------------------------------------------- | |
// Constants | |
// ------------------------------------------------------------------------------------------------- | |
const FIELD_COLOR: Color = Color::BLACK; | |
const OBJ_COLOR: Color = Color::WHITE; | |
const DRAW_MODE: DrawMode = DrawMode::Fill(FillOptions::DEFAULT); | |
const RACKET_HEIGHT: f32 = 100.0; | |
const RACKET_WIDTH: f32 = 20.0; | |
const RACKET_SPEED_MULTIPLIER: f32 = 500.0; | |
const BALL_RADIUS: f32 = 15.0; | |
const BALL_SPEED_MULTIPLIER: f32 = 500.0; | |
// ------------------------------------------------------------------------------------------------- | |
// State | |
// ------------------------------------------------------------------------------------------------- | |
struct Field { | |
score: (u32, u32), | |
width: f32, | |
height: f32, | |
center: mint::Point2<f32>, | |
mesh: Mesh, | |
} | |
impl Field { | |
fn new(_ctx: &mut Context, dimensions: (f32, f32)) -> GameResult<Self> { | |
let (width, height) = dimensions; | |
let center = mint::Point2 { | |
x: width * 0.5, | |
y: height * 0.5, | |
}; | |
let rect = Rect::new(-0.5, -height * 0.5, 1.0, height); | |
let mesh = Mesh::new_rectangle(_ctx, DRAW_MODE, rect, OBJ_COLOR)?; | |
Ok(Field { | |
score: (0, 0), | |
width, | |
height, | |
center, | |
mesh, | |
}) | |
} | |
fn draw(&self, context: &mut Context) -> GameResult { | |
let line_draw_params = DrawParam::default().dest(self.center); | |
graphics::draw(context, &self.mesh, line_draw_params)?; | |
let score_text = graphics::Text::new(format!("{} {}", self.score.0, self.score.1)); | |
let score_text_width = score_text.dimensions(context).w; | |
let score_draw_params = DrawParam::default().dest(mint::Point2 { | |
x: self.center.x - score_text_width * 0.5, | |
y: 0.0, | |
}); | |
graphics::draw(context, &score_text, score_draw_params) | |
} | |
fn fit(&self, position: mint::Point2<f32>, width: f32, height: f32) -> mint::Point2<f32> { | |
let x: f32 = if position.x - width * 0.5 < 0.0 { | |
width * 0.5 | |
} else if position.x + width * 0.5 > self.width { | |
self.width - width * 0.5 | |
} else { | |
position.x | |
}; | |
let y: f32 = if position.y - height * 0.5 < 0.0 { | |
height * 0.5 | |
} else if position.y + height * 0.5 > self.height { | |
self.height - height * 0.5 | |
} else { | |
position.y | |
}; | |
mint::Point2 { x, y } | |
} | |
} | |
struct Racket { | |
width: f32, | |
height: f32, | |
position: mint::Point2<f32>, | |
mesh: Mesh, | |
} | |
impl Racket { | |
fn new(_ctx: &mut Context, width: f32, height: f32, x: f32, y: f32) -> GameResult<Self> { | |
let rect = Rect::new(-width * 0.5, -height * 0.5, width, height); | |
let mesh = Mesh::new_rectangle(_ctx, DRAW_MODE, rect, OBJ_COLOR)?; | |
let position = mint::Point2 { x, y }; | |
Ok(Racket { | |
width, | |
height, | |
position, | |
mesh, | |
}) | |
} | |
fn draw(&self, context: &mut Context) -> GameResult { | |
let draw_params = DrawParam::default().dest(self.position); | |
graphics::draw(context, &self.mesh, draw_params) | |
} | |
fn handle_controls( | |
&mut self, | |
_ctx: &Context, | |
field: &Field, | |
up: KeyCode, | |
down: KeyCode, | |
) -> GameResult { | |
let delta_time = ggez::timer::delta(_ctx).as_secs_f32(); | |
if keyboard::is_key_pressed(_ctx, up) { | |
self.position.y -= 1.0 * RACKET_SPEED_MULTIPLIER * delta_time; | |
} | |
if keyboard::is_key_pressed(_ctx, down) { | |
self.position.y += 1.0 * RACKET_SPEED_MULTIPLIER * delta_time; | |
} | |
self.position = field.fit(self.position, self.width, self.height); | |
Ok(()) | |
} | |
} | |
struct Ball { | |
radius: f32, | |
position: mint::Point2<f32>, | |
direction: mint::Vector2<f32>, | |
mesh: Mesh, | |
} | |
impl Ball { | |
fn new(context: &mut Context, radius: f32, position: mint::Point2<f32>) -> GameResult<Self> { | |
let rect = Rect::new(-radius, -radius, radius * 2.0, radius * 2.0); | |
let mesh = Mesh::new_rectangle(context, DRAW_MODE, rect, OBJ_COLOR)?; | |
let direction = mint::Vector2 { x: 0.0, y: 0.0 }; | |
let mut ball = Ball { | |
radius, | |
position, | |
direction, | |
mesh, | |
}; | |
ball.reset(position); | |
Ok(ball) | |
} | |
fn draw(&self, context: &mut Context) -> GameResult { | |
let draw_params = DrawParam::default().dest(self.position); | |
graphics::draw(context, &self.mesh, draw_params) | |
} | |
fn reset(&mut self, position: mint::Point2<f32>) { | |
let (x, y): (f32, f32) = (random(), random()); | |
let direction = mint::Vector2 { | |
x: if random() { x + 0.1 } else { x.neg() - 0.1 }, | |
y: if random() { y } else { y.neg() }, | |
}; | |
self.position = position; | |
self.direction = direction; | |
} | |
fn advance( | |
&mut self, | |
_ctx: &Context, | |
field: &mut Field, | |
racket_1: &Racket, | |
racket_2: &Racket, | |
) -> GameResult { | |
let delta_time = ggez::timer::delta(_ctx).as_secs_f32(); | |
self.position.y += self.direction.y * delta_time * BALL_SPEED_MULTIPLIER; | |
self.position.x += self.direction.x * delta_time * BALL_SPEED_MULTIPLIER; | |
self.position = field.fit(self.position, self.radius * 2.0, self.radius * 2.0); | |
// reflect from rackets | |
let rocket_1_reflect = self.direction.x < 0.0 | |
&& self.position.x - self.radius <= racket_1.position.x + racket_1.width * 0.5 | |
&& (self.position.y + self.radius >= racket_1.position.y - racket_1.height * 0.5 | |
&& self.position.y + self.radius <= racket_1.position.y + racket_1.height * 0.5 | |
|| self.position.y - self.radius >= racket_1.position.y - racket_1.height * 0.5 | |
&& self.position.y - self.radius | |
<= racket_1.position.y + racket_1.height * 0.5); | |
let rocket_2_reflect = self.direction.x > 0.0 | |
&& self.position.x + self.radius >= racket_2.position.x - racket_2.width * 0.5 | |
&& (self.position.y + self.radius >= racket_2.position.y - racket_2.height * 0.5 | |
&& self.position.y + self.radius <= racket_2.position.y + racket_2.height * 0.5 | |
|| self.position.y - self.radius >= racket_2.position.y - racket_2.height * 0.5 | |
&& self.position.y - self.radius | |
<= racket_2.position.y + racket_2.height * 0.5); | |
if rocket_1_reflect || rocket_2_reflect { | |
self.direction.x = self.direction.x.neg(); | |
self.direction.y = random(); | |
return Ok(()); | |
} | |
// score and reset if crossed left or right border | |
if self.position.x + self.radius == field.width { | |
field.score.0 += 1; | |
self.reset(field.center); | |
return Ok(()); | |
} | |
if self.position.x - self.radius == 0.0 { | |
field.score.1 += 1; | |
self.reset(field.center); | |
return Ok(()); | |
} | |
// reflect from top and bottom | |
if self.position.y + self.radius == field.height { | |
self.direction.y = self.direction.y.neg(); | |
return Ok(()); | |
} | |
if self.position.y - self.radius == 0.0 { | |
self.direction.y = self.direction.y.neg(); | |
return Ok(()); | |
} | |
Ok(()) | |
} | |
} | |
struct State { | |
field: Field, | |
racket_1: Racket, | |
racket_2: Racket, | |
ball: Ball, | |
} | |
impl State { | |
pub fn new(_ctx: &mut Context) -> GameResult<Self> { | |
let field = Field::new(_ctx, graphics::drawable_size(_ctx))?; | |
let racket_1 = Racket::new( | |
_ctx, | |
RACKET_WIDTH, | |
RACKET_HEIGHT, | |
RACKET_WIDTH * 0.5, | |
field.center.y, | |
)?; | |
let racket_2 = Racket::new( | |
_ctx, | |
RACKET_WIDTH, | |
RACKET_HEIGHT, | |
field.width - RACKET_WIDTH * 0.5, | |
field.center.y, | |
)?; | |
let ball = Ball::new(_ctx, BALL_RADIUS, field.center)?; | |
Ok(State { | |
field, | |
racket_1, | |
racket_2, | |
ball, | |
}) | |
} | |
} | |
impl EventHandler<GameError> for State { | |
fn update(&mut self, _ctx: &mut Context) -> Result<(), GameError> { | |
self.racket_1 | |
.handle_controls(_ctx, &self.field, KeyCode::W, KeyCode::S)?; | |
self.racket_2 | |
.handle_controls(_ctx, &self.field, KeyCode::Up, KeyCode::Down)?; | |
self.ball | |
.advance(_ctx, &mut self.field, &self.racket_1, &self.racket_2)?; | |
Ok(()) | |
} | |
fn draw(&mut self, _ctx: &mut Context) -> Result<(), GameError> { | |
graphics::clear(_ctx, FIELD_COLOR); | |
self.field.draw(_ctx)?; | |
self.racket_1.draw(_ctx)?; | |
self.racket_2.draw(_ctx)?; | |
self.ball.draw(_ctx)?; | |
graphics::present(_ctx) | |
} | |
} | |
// ------------------------------------------------------------------------------------------------- | |
// Main | |
// ------------------------------------------------------------------------------------------------- | |
fn main() -> GameResult { | |
let context_builder = ContextBuilder::new("pong", "xdcrafts"); | |
let (mut ctx, event_loop) = context_builder | |
.build()?; | |
graphics::set_window_title(&ctx, "Pong"); | |
let state = State::new(&mut ctx)?; | |
event::run(ctx, event_loop, state) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment