macroquad_platformer/
lib.rs

1use macroquad::math::{vec2, Rect, Vec2};
2
3use std::collections::HashSet;
4
5#[derive(Debug, PartialEq, Clone, Copy)]
6pub enum Tile {
7    Empty,
8    Solid,
9    JumpThrough,
10    Collider,
11}
12
13impl Tile {
14    fn or(self, other: Tile) -> Tile {
15        match (self, other) {
16            (Tile::Empty, Tile::Empty) => Tile::Empty,
17            (Tile::JumpThrough, Tile::JumpThrough) => Tile::JumpThrough,
18            (Tile::JumpThrough, Tile::Empty) => Tile::JumpThrough,
19            (Tile::Empty, Tile::JumpThrough) => Tile::JumpThrough,
20            _ => Tile::Solid,
21        }
22    }
23}
24pub struct StaticTiledLayer {
25    static_colliders: Vec<Tile>,
26    tile_width: f32,
27    tile_height: f32,
28    width: usize,
29    tag: u8,
30}
31
32pub struct World {
33    static_tiled_layers: Vec<StaticTiledLayer>,
34    solids: Vec<(Solid, Collider)>,
35    actors: Vec<(Actor, Collider)>,
36}
37
38#[derive(Clone, Debug)]
39struct Collider {
40    collidable: bool,
41    squished: bool,
42    pos: Vec2,
43    width: i32,
44    height: i32,
45    x_remainder: f32,
46    y_remainder: f32,
47    squishers: HashSet<Solid>,
48    descent: bool,
49    seen_wood: bool,
50}
51
52impl Collider {
53    pub fn rect(&self) -> Rect {
54        Rect::new(
55            self.pos.x,
56            self.pos.y,
57            self.width as f32,
58            self.height as f32,
59        )
60    }
61}
62
63#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
64pub struct Actor(usize);
65
66#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
67pub struct Solid(usize);
68
69impl World {
70    pub fn new() -> World {
71        World {
72            static_tiled_layers: vec![],
73            actors: vec![],
74            solids: vec![],
75        }
76    }
77
78    pub fn add_static_tiled_layer(
79        &mut self,
80        static_colliders: Vec<Tile>,
81        tile_width: f32,
82        tile_height: f32,
83        width: usize,
84        tag: u8,
85    ) {
86        self.static_tiled_layers.push(StaticTiledLayer {
87            static_colliders,
88            tile_width,
89            tile_height,
90            width,
91            tag,
92        });
93    }
94    pub fn add_actor(&mut self, pos: Vec2, width: i32, height: i32) -> Actor {
95        let actor = Actor(self.actors.len());
96
97        let mut descent = false;
98        let mut seen_wood = false;
99        let tile = self.collide_solids(pos, width, height);
100        if tile == Tile::JumpThrough {
101            descent = true;
102            seen_wood = true;
103        }
104        self.actors.push((
105            actor,
106            Collider {
107                collidable: true,
108                squished: false,
109                pos,
110                width,
111                height,
112                x_remainder: 0.,
113                y_remainder: 0.,
114                squishers: HashSet::new(),
115                descent,
116                seen_wood,
117            },
118        ));
119
120        actor
121    }
122
123    pub fn add_solid(&mut self, pos: Vec2, width: i32, height: i32) -> Solid {
124        let solid = Solid(self.solids.len());
125
126        self.solids.push((
127            solid,
128            Collider {
129                collidable: true,
130                squished: false,
131                pos,
132                width,
133                height,
134                x_remainder: 0.,
135                y_remainder: 0.,
136                squishers: HashSet::new(),
137                descent: false,
138                seen_wood: false,
139            },
140        ));
141
142        solid
143    }
144
145    pub fn set_actor_position(&mut self, actor: Actor, pos: Vec2) {
146        let collider = &mut self.actors[actor.0].1;
147
148        collider.x_remainder = 0.0;
149        collider.y_remainder = 0.0;
150        collider.pos = pos;
151    }
152
153    pub fn descent(&mut self, actor: Actor) {
154        let collider = &mut self.actors[actor.0].1;
155        collider.descent = true;
156    }
157
158    pub fn move_v(&mut self, actor: Actor, dy: f32) -> bool {
159        let id = actor.0;
160        let mut collider = self.actors[id].1.clone();
161
162        collider.y_remainder += dy;
163
164        let mut move_ = collider.y_remainder.round() as i32;
165        if move_ != 0 {
166            collider.y_remainder -= move_ as f32;
167            let sign = move_.signum();
168
169            while move_ != 0 {
170                let tile = self.collide_solids(
171                    collider.pos + vec2(0., sign as f32),
172                    collider.width,
173                    collider.height,
174                );
175
176                // collider wants to go down and collided with jumpthrough tile
177                if tile == Tile::JumpThrough && collider.descent {
178                    collider.seen_wood = true;
179                }
180                // collider wants to go up and encoutered jumpthrough obstace
181                if tile == Tile::JumpThrough && sign < 0 {
182                    collider.seen_wood = true;
183                    collider.descent = true;
184                }
185                if tile == Tile::Empty || (tile == Tile::JumpThrough && collider.descent) {
186                    collider.pos.y += sign as f32;
187                    move_ -= sign;
188                } else {
189                    self.actors[id].1 = collider;
190
191                    return false;
192                }
193            }
194        }
195
196        // Final check, if we are out of woods after the move - reset wood flags
197        let tile = self.collide_solids(collider.pos, collider.width, collider.height);
198        if tile != Tile::JumpThrough {
199            collider.seen_wood = false;
200            collider.descent = false;
201        }
202
203        self.actors[id].1 = collider;
204        true
205    }
206
207    pub fn move_h(&mut self, actor: Actor, dx: f32) -> bool {
208        let id = actor.0;
209        let mut collider = self.actors[id].1.clone();
210        collider.x_remainder += dx;
211
212        let mut move_ = collider.x_remainder.round() as i32;
213        if move_ != 0 {
214            collider.x_remainder -= move_ as f32;
215            let sign = move_.signum();
216
217            while move_ != 0 {
218                let tile = self.collide_solids(
219                    collider.pos + vec2(sign as f32, 0.),
220                    collider.width,
221                    collider.height,
222                );
223                if tile == Tile::JumpThrough {
224                    collider.descent = true;
225                    collider.seen_wood = true;
226                }
227                if tile == Tile::Empty || tile == Tile::JumpThrough {
228                    collider.pos.x += sign as f32;
229                    move_ -= sign;
230                } else {
231                    self.actors[id].1 = collider;
232                    return false;
233                }
234            }
235        }
236        self.actors[id].1 = collider;
237        true
238    }
239
240    pub fn solid_move(&mut self, solid: Solid, dx: f32, dy: f32) {
241        let collider = &mut self.solids[solid.0].1;
242
243        collider.x_remainder += dx;
244        collider.y_remainder += dy;
245        let move_x = collider.x_remainder.round() as i32;
246        let move_y = collider.y_remainder.round() as i32;
247
248        let mut riding_actors = vec![];
249        let mut pushing_actors = vec![];
250
251        let riding_rect = Rect::new(
252            collider.pos.x,
253            collider.pos.y - 1.0,
254            collider.width as f32,
255            1.0,
256        );
257        let pushing_rect = Rect::new(
258            collider.pos.x + move_x as f32,
259            collider.pos.y,
260            collider.width as f32,
261            collider.height as f32,
262        );
263
264        for (actor, actor_collider) in &mut self.actors {
265            let rider_rect = Rect::new(
266                actor_collider.pos.x,
267                actor_collider.pos.y + actor_collider.height as f32 - 1.0,
268                actor_collider.width as f32,
269                1.0,
270            );
271
272            if riding_rect.overlaps(&rider_rect) {
273                riding_actors.push(*actor);
274            } else if pushing_rect.overlaps(&actor_collider.rect())
275                && actor_collider.squished == false
276            {
277                pushing_actors.push(*actor);
278            }
279
280            if pushing_rect.overlaps(&actor_collider.rect()) == false {
281                actor_collider.squishers.remove(&solid);
282                if actor_collider.squishers.len() == 0 {
283                    actor_collider.squished = false;
284                }
285            }
286        }
287
288        self.solids[solid.0].1.collidable = false;
289        for actor in riding_actors {
290            self.move_h(actor, move_x as f32);
291        }
292        for actor in pushing_actors {
293            let squished = !self.move_h(actor, move_x as f32);
294            if squished {
295                self.actors[actor.0].1.squished = true;
296                self.actors[actor.0].1.squishers.insert(solid);
297            }
298        }
299        self.solids[solid.0].1.collidable = true;
300
301        let collider = &mut self.solids[solid.0].1;
302        if move_x != 0 {
303            collider.x_remainder -= move_x as f32;
304            collider.pos.x += move_x as f32;
305        }
306        if move_y != 0 {
307            collider.y_remainder -= move_y as f32;
308            collider.pos.y += move_y as f32;
309        }
310    }
311
312    pub fn solid_at(&self, pos: Vec2) -> bool {
313        self.tag_at(pos, 1)
314    }
315
316    pub fn tag_at(&self, pos: Vec2, tag: u8) -> bool {
317        for StaticTiledLayer {
318            tile_width,
319            tile_height,
320            width,
321            static_colliders,
322            tag: layer_tag,
323        } in &self.static_tiled_layers
324        {
325            let y = (pos.y / tile_width) as i32;
326            let x = (pos.x / tile_height) as i32;
327            let ix = y * (*width as i32) + x;
328
329            if ix >= 0
330                && ix < static_colliders.len() as i32
331                && static_colliders[ix as usize] != Tile::Empty
332            {
333                return *layer_tag == tag;
334            }
335        }
336
337        self.solids
338            .iter()
339            .any(|solid| solid.1.collidable && solid.1.rect().contains(pos))
340    }
341
342    pub fn collide_solids(&self, pos: Vec2, width: i32, height: i32) -> Tile {
343        let tile = self.collide_tag(1, pos, width, height);
344        if tile != Tile::Empty {
345            return tile;
346        }
347
348        self.solids
349            .iter()
350            .find(|solid| {
351                solid.1.collidable
352                    && solid.1.rect().overlaps(&Rect::new(
353                        pos.x,
354                        pos.y,
355                        width as f32,
356                        height as f32,
357                    ))
358            })
359            .map_or(Tile::Empty, |_| Tile::Collider)
360    }
361
362    pub fn collide_tag(&self, tag: u8, pos: Vec2, width: i32, height: i32) -> Tile {
363        for StaticTiledLayer {
364            tile_width,
365            tile_height,
366            width: layer_width,
367            static_colliders,
368            tag: layer_tag,
369        } in &self.static_tiled_layers
370        {
371            let layer_height = static_colliders.len() / layer_width + 1;
372            let check = |pos: Vec2| {
373                let y = (pos.y / tile_width) as i32;
374                let x = (pos.x / tile_height) as i32;
375                let ix = y * (*layer_width as i32) + x;
376                if y >= 0
377                    && y < layer_height as i32
378                    && x >= 0
379                    && x < *layer_width as i32
380                    && ix >= 0
381                    && ix < static_colliders.len() as i32
382                    && *layer_tag == tag
383                    && static_colliders[ix as usize] != Tile::Empty
384                {
385                    return static_colliders[ix as usize];
386                }
387                return Tile::Empty;
388            };
389
390            let tile = check(pos)
391                .or(check(pos + vec2(width as f32 - 1.0, 0.0)))
392                .or(check(pos + vec2(width as f32 - 1.0, height as f32 - 1.0)))
393                .or(check(pos + vec2(0.0, height as f32 - 1.0)));
394
395            if tile != Tile::Empty {
396                return tile;
397            }
398
399            if width > *tile_width as i32 {
400                let mut x = pos.x;
401
402                while {
403                    x += tile_width;
404                    x < pos.x + width as f32 - 1.
405                } {
406                    let tile =
407                        check(vec2(x, pos.y)).or(check(vec2(x, pos.y + height as f32 - 1.0)));
408                    if tile != Tile::Empty {
409                        return tile;
410                    }
411                }
412            }
413
414            if height > *tile_height as i32 {
415                let mut y = pos.y;
416
417                while {
418                    y += tile_height;
419                    y < pos.y + height as f32 - 1.
420                } {
421                    let tile = check(vec2(pos.x, y)).or(check(vec2(pos.x + width as f32 - 1., y)));
422                    if tile != Tile::Empty {
423                        return tile;
424                    }
425                }
426            }
427        }
428        return Tile::Empty;
429    }
430
431    pub fn squished(&self, actor: Actor) -> bool {
432        self.actors[actor.0].1.squished
433    }
434
435    pub fn actor_pos(&self, actor: Actor) -> Vec2 {
436        self.actors[actor.0].1.pos
437    }
438
439    pub fn solid_pos(&self, solid: Solid) -> Vec2 {
440        self.solids[solid.0].1.pos
441    }
442
443    pub fn collide_check(&self, collider: Actor, pos: Vec2) -> bool {
444        let collider = &self.actors[collider.0];
445
446        let tile = self.collide_solids(pos, collider.1.width, collider.1.height);
447        if collider.1.descent {
448            tile == Tile::Solid || tile == Tile::Collider
449        } else {
450            tile == Tile::Solid || tile == Tile::Collider || tile == Tile::JumpThrough
451        }
452    }
453}