Compare commits

...

14 Commits

11 changed files with 482 additions and 80 deletions

15
Cargo.lock generated
View File

@@ -2,13 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "Pac-Man"
version = "0.1.0"
dependencies = [
"sdl2",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -33,6 +26,14 @@ version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "pacman"
version = "0.1.0"
dependencies = [
"lazy_static",
"sdl2",
]
[[package]]
name = "sdl2"
version = "0.35.2"

View File

@@ -1,9 +1,10 @@
[package]
name = "Pac-Man"
name = "pacman"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lazy_static = "1.4.0"
sdl2 = { version = "0.35", features = ["image", "ttf", "mixer"] }

35
IMPLEMENTATION.md Normal file
View File

@@ -0,0 +1,35 @@
# Implementation
A document detailing the implementation the project from rendering, to game logic, to build systems.
## Rendering
1. Map
- May require procedural text generation later on (cacheable?)
2. Pacman
3. Ghosts
- Requires colors
4. Items
5. Interface
- Requires fonts
## Grid System
1. How does the grid system work?
The grid is 28 x 36 (although, the map texture is 28 x 37), and each cell is 24x24 (pixels).
Many of the walls in the map texture only occupy a portion of the cell, so some items are able to render across multiple cells.
24x24 assets include pellets, the energizer, and the map itself ()
2. What constraints must be enforced on Ghosts and PacMan?
3. How do movement transitions work?
All entities store a precise position, and a direction. This position is only used for animation, rendering, and collision purposes. Otherwise, a separate 'cell position' (which is 24 times less precise, owing to the fact that it is based on the entity's position within the grid).
When an entity is transitioning between cells, movement directions are acknowledged, but won't take effect until the next cell has been entered completely.
4. Between transitions, how does collision detection work?
It appears the original implementation used cell-level detection.
I worry this may be prone to division errors. Make sure to use rounding (50% >=).

90
src/animation.rs Normal file
View File

@@ -0,0 +1,90 @@
use sdl2::{
rect::Rect,
render::{Canvas, Texture},
video::Window,
};
use crate::direction::Direction;
pub struct AnimatedTexture<'a> {
raw_texture: Texture<'a>,
ticker: u32,
reversed: bool,
ticks_per_frame: u32,
frame_count: u32,
frame_width: u32,
frame_height: u32,
}
impl<'a> AnimatedTexture<'a> {
pub fn new(
texture: Texture<'a>,
ticks_per_frame: u32,
frame_count: u32,
frame_width: u32,
frame_height: u32,
) -> Self {
AnimatedTexture {
raw_texture: texture,
ticker: 0,
reversed: false,
ticks_per_frame,
frame_count,
frame_width,
frame_height,
}
}
fn current_frame(&self) -> u32 {
self.ticker / self.ticks_per_frame
}
fn next_frame(&mut self) {
if self.reversed {
self.ticker -= 1;
if self.ticker == 0 {
self.reversed = !self.reversed;
}
} else {
self.ticker += 1;
if self.ticker + 1 == self.ticks_per_frame * self.frame_count {
self.reversed = !self.reversed;
}
}
}
fn get_frame_rect(&self) -> Rect {
Rect::new(
self.current_frame() as i32 * self.frame_width as i32,
0,
self.frame_width,
self.frame_height,
)
}
pub fn render(
&mut self,
canvas: &mut Canvas<Window>,
position: (i32, i32),
direction: Direction,
) {
let frame_rect = self.get_frame_rect();
let position_rect = Rect::new(position.0, position.1, self.frame_width, self.frame_height);
canvas
.copy_ex(
&self.raw_texture,
Some(frame_rect),
Some(position_rect),
direction.angle(),
None,
false,
false,
)
.expect("Could not render texture on canvas");
self.next_frame();
}
}

View File

@@ -1,44 +1,95 @@
use lazy_static::lazy_static;
pub const BOARD_WIDTH: u32 = 28;
pub const BOARD_HEIGHT: u32 = 36;
pub const BLOCK_SIZE_24: u32 = 24;
pub const BLOCK_SIZE_32: u32 = 32;
pub const BOARD_HEIGHT: u32 = 37; // Adjusted to fit map texture?
pub const CELL_SIZE: u32 = 24;
pub const WINDOW_WIDTH: u32 = BLOCK_SIZE_24 * BOARD_WIDTH;
pub const WINDOW_HEIGHT: u32 = BLOCK_SIZE_24 * BOARD_HEIGHT;
pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH;
pub const WINDOW_HEIGHT: u32 = CELL_SIZE * BOARD_HEIGHT;
pub const RAW_BOARD: &str = r###"
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum MapTile {
Empty,
Wall,
Pellet,
PowerPellet,
StartingPosition(u8),
}
pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
" ",
" ",
" ",
"############################",
"#............##............#",
"#.####.#####.##.#####.####.#",
"#o####.#####.##.#####.####o#",
"#.####.#####.##.#####.####.#",
"#..........................#",
"#.####.##.########.##.####.#",
"#.####.##.########.##.####.#",
"#......##....##....##......#",
"######.##### ## #####.######",
" #.##### ## #####.# ",
" #.## 1 ##.# ",
" #.## ###==### ##.# ",
"######.## # # ##.######",
" . #2 3 4 # . ",
"######.## # # ##.######",
" #.## ######## ##.# ",
" #.## ##.# ",
" #.## ######## ##.# ",
"######.## ######## ##.######",
"#............##............#",
"#.####.#####.##.#####.####.#",
"#.####.#####.##.#####.####.#",
"#o..##.......0 .......##..o#",
"###.##.##.########.##.##.###",
"###.##.##.########.##.##.###",
"#......##....##....##......#",
"#.##########.##.##########.#",
"#.##########.##.##########.#",
"#..........................#",
"############################",
" ",
" ",
" ",
];
############################
#............##............#
#.####.#####.##.#####.####.#
#o####.#####.##.#####.####o#
#.####.#####.##.#####.####.#
#..........................#
#.####.##.########.##.####.#
#.####.##.########.##.####.#
#......##....##....##......#
######.##### ## #####.######
#.##### ## #####.#
#.## 1 ##.#
#.## ###==### ##.#
######.## # # ##.######
. #2 3 4 # .
######.## # # ##.######
#.## ######## ##.#
#.## ##.#
#.## ######## ##.#
######.## ######## ##.######
#............##............#
#.####.#####.##.#####.####.#
#.####.#####.##.#####.####.#
#o..##.......0 .......##..o#
###.##.##.########.##.##.###
###.##.##.########.##.##.###
#......##....##....##......#
#.##########.##.##########.#
#.##########.##.##########.#
#..........................#
############################
lazy_static! {
pub static ref BOARD: [[MapTile; BOARD_HEIGHT as usize]; BOARD_HEIGHT as usize] = {
let mut board = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_HEIGHT as usize];
"###;
for y in 0..BOARD_HEIGHT as usize {
let line = RAW_BOARD[y];
for x in 0..BOARD_WIDTH as usize {
if x >= line.len() {
break;
}
let i = (y * (BOARD_WIDTH as usize) + x) as usize;
let character = line
.chars()
.nth(x as usize)
.unwrap_or_else(|| panic!("Could not get character at {} = ({}, {})", i, x, y));
let tile = match character {
'#' => MapTile::Wall,
'.' => MapTile::Pellet,
'o' => MapTile::PowerPellet,
' ' => MapTile::Empty,
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => {
MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)
},
'=' => MapTile::Empty,
_ => panic!("Unknown character in board: {}", character),
};
board[x as usize][y as usize] = tile;
}
}
board
};
}

18
src/direction.rs Normal file
View File

@@ -0,0 +1,18 @@
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
impl Direction {
pub fn angle(&self) -> f64 {
match self {
Direction::Right => 0f64,
Direction::Down => 90f64,
Direction::Left => 180f64,
Direction::Up => 270f64,
}
}
}

10
src/entity.rs Normal file
View File

@@ -0,0 +1,10 @@
pub trait Entity {
// Returns true if the entity is colliding with the other entity
fn is_colliding(&self, other: &dyn Entity) -> bool;
// Returns the absolute position of the entity
fn position(&self) -> (i32, i32);
// Returns the cell position of the entity (XY position within the grid)
fn cell_position(&self) -> (u32, u32);
// Tick the entity (move it, perform collision checks, etc)
fn tick(&mut self);
}

View File

@@ -1,11 +1,99 @@
pub struct Game {}
use sdl2::image::LoadTexture;
use sdl2::keyboard::Keycode;
use sdl2::render::{TextureCreator, Texture};
use sdl2::video::WindowContext;
use sdl2::{pixels::Color, render::Canvas, video::Window};
impl Game {
pub fn new() -> Game {
Game {}
use crate::constants::{MapTile, BOARD, BOARD_HEIGHT, BOARD_WIDTH};
use crate::direction::Direction;
use crate::entity::Entity;
use crate::pacman::{Pacman};
pub struct Game<'a> {
canvas: &'a mut Canvas<Window>,
map_texture: Texture<'a>,
pacman: Pacman<'a>,
debug: bool,
}
pub fn tick() {}
impl Game<'_> {
pub fn new<'a>(
canvas: &'a mut Canvas<Window>,
texture_creator: &'a TextureCreator<WindowContext>,
) -> Game<'a> {
let pacman_atlas = texture_creator
.load_texture("assets/32/pacman.png")
.expect("Could not load pacman texture");
let pacman = Pacman::new(None, pacman_atlas);
pub fn draw() {}
Game {
canvas,
pacman: pacman,
debug: true,
map_texture: texture_creator
.load_texture("assets/map.png")
.expect("Could not load pacman texture"),
}
}
pub fn keyboard_event(&mut self, keycode: Keycode) {
match keycode {
Keycode::D => {
self.pacman.direction = Direction::Right;
}
Keycode::A => {
self.pacman.direction = Direction::Left;
}
Keycode::W => {
self.pacman.direction = Direction::Up;
}
Keycode::S => {
self.pacman.direction = Direction::Down;
}
Keycode::Space => {
self.debug = !self.debug;
}
_ => {}
}
}
pub fn tick(&mut self) {
self.pacman.tick();
}
pub fn draw(&mut self) {
// Clear the screen (black)
self.canvas.set_draw_color(Color::RGB(0, 0, 0));
self.canvas.clear();
self.canvas
.copy(&self.map_texture, None, None)
.expect("Could not render texture on canvas");
// Render the pacman
self.pacman.render(self.canvas);
// Draw a grid
for x in 0..BOARD_WIDTH {
for y in 0..BOARD_HEIGHT {
let tile = BOARD[x as usize][y as usize];
let color = match tile {
MapTile::Empty => None,
MapTile::Wall => Some(Color::BLUE),
MapTile::Pellet => Some(Color::RED),
MapTile::PowerPellet => Some(Color::MAGENTA),
MapTile::StartingPosition(_) => Some(Color::GREEN),
};
if let Some(color) = color {
self.canvas.set_draw_color(color);
self.canvas
.draw_rect(sdl2::rect::Rect::new(x as i32 * 24, y as i32 * 24, 24, 24))
.expect("Could not draw rectangle");
}
}
}
self.canvas.present();
}
}

View File

@@ -1,17 +1,22 @@
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
use sdl2::event::{Event, WindowEvent};
use sdl2::image::LoadTexture;
use crate::game::Game;
use crate::textures::TextureManager;
use sdl2::event::{Event};
use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;
use sdl2::render::{Texture, Canvas};
use std::time::Duration;
use sdl2::render::{Canvas, Texture};
use std::time::{Duration, Instant};
#[cfg(target_os = "emscripten")]
pub mod emscripten;
mod board;
mod constants;
mod direction;
mod game;
mod pacman;
mod textures;
mod entity;
mod animation;
fn redraw(canvas: &mut Canvas<sdl2::video::Window>, tex: &Texture, i: u8) {
canvas.set_draw_color(Color::RGB(i, i, i));
@@ -28,7 +33,6 @@ pub fn main() {
let window = video_subsystem
.window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT)
.position_centered()
.resizable()
.build()
.expect("Could not initialize window");
@@ -36,23 +40,26 @@ pub fn main() {
.into_canvas()
.build()
.expect("Could not build canvas");
let texture_creator = canvas.texture_creator();
let map_texture = texture_creator
.load_texture("assets/map.png")
.expect("Could not load pacman texture");
canvas
.copy(&map_texture, None, None)
.expect("Could not render texture on canvas");
.set_logical_size(WINDOW_WIDTH, WINDOW_HEIGHT)
.expect("Could not set logical size");
let mut i = 0u8;
let texture_creator = canvas.texture_creator();
let mut game = Game::new(&mut canvas, &texture_creator);
let mut event_pump = sdl_context
.event_pump()
.expect("Could not get SDL EventPump");
game.draw();
game.tick();
let loop_time = Duration::from_millis(1000 / 60);
let mut main_loop = || {
let start = Instant::now();
for event in event_pump.poll_iter() {
match event {
// Handle quitting keys or window close
@@ -61,23 +68,31 @@ pub fn main() {
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
..
} => return false,
event @ Event::KeyDown { .. } => {
println!("{:?}", event);
},
Event::Window { win_event, .. } => {
if let WindowEvent::Resized(width, height) = win_event {
i = i.wrapping_add(1);
canvas.set_logical_size(width as u32, height as u32).unwrap();
redraw(&mut canvas, &map_texture, i);
}
Event::KeyDown { keycode , .. } => {
game.keyboard_event(keycode.unwrap());
},
_ => {}
}
}
canvas.present();
::std::thread::sleep(Duration::from_millis(10));
let tick_time = {
let start = Instant::now();
game.tick();
start.elapsed()
};
let draw_time = {
let start = Instant::now();
game.draw();
start.elapsed()
};
if start.elapsed() < loop_time {
::std::thread::sleep(loop_time - start.elapsed());
} else {
println!("Game loop behind schedule by: {:?}", start.elapsed() - loop_time);
}
true
};

64
src/pacman.rs Normal file
View File

@@ -0,0 +1,64 @@
use sdl2::{
render::{Canvas, Texture},
video::Window,
};
use crate::{animation::AnimatedTexture, direction::Direction, entity::Entity};
pub struct Pacman<'a> {
// Absolute position on the board (precise)
pub position: (i32, i32),
pub direction: Direction,
speed: u32,
sprite: AnimatedTexture<'a>,
}
impl Pacman<'_> {
pub fn new<'a>(starting_position: Option<(i32, i32)>, atlas: Texture<'a>) -> Pacman<'a> {
Pacman {
position: starting_position.unwrap_or((0i32, 0i32)),
direction: Direction::Right,
speed: 2,
sprite: AnimatedTexture::new(atlas, 4, 3, 32, 32),
}
}
pub fn render(&mut self, canvas: &mut Canvas<Window>) {
self.sprite.render(canvas, self.position, self.direction);
}
}
impl Entity for Pacman<'_> {
fn is_colliding(&self, other: &dyn Entity) -> bool {
let (x, y) = self.position();
let (other_x, other_y) = other.position();
x == other_x && y == other_y
}
fn position(&self) -> (i32, i32) {
self.position
}
fn cell_position(&self) -> (u32, u32) {
let (x, y) = self.position();
(x as u32 / 24, y as u32 / 24)
}
fn tick(&mut self) {
let speed = self.speed as i32;
match self.direction {
Direction::Right => {
self.position.0 += speed;
}
Direction::Left => {
self.position.0 -= speed;
}
Direction::Up => {
self.position.1 -= speed;
}
Direction::Down => {
self.position.1 += speed;
}
}
}
}

29
src/textures.rs Normal file
View File

@@ -0,0 +1,29 @@
use sdl2::{
image::LoadTexture,
render::{Texture, TextureCreator},
video::WindowContext,
};
pub struct TextureManager<'a> {
pub map: Texture<'a>,
pub pacman: Texture<'a>,
}
impl<'a> TextureManager<'a> {
pub fn new(texture_creator: &'a TextureCreator<WindowContext>) -> Self {
let map_texture = texture_creator
.load_texture("assets/map.png")
.expect("Could not load pacman texture");
let pacman_atlas = texture_creator
.load_texture("assets/pacman.png")
.expect("Could not load pacman texture");
TextureManager {
map: map_texture,
pacman: pacman_atlas,
}
}
}