fyrox_impl/scene/tilemap/
tile_collider.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Tile colliders provide shapes for tiles so that physics colliders may be automatically
22//! constructed for tile maps. [`TileCollider`] divides the colliders into broad categories,
23//! including no shape and a shape that covers the full tile, while [`CustomTileCollider`]
24//! is a resource that contains triangles to allow an arbitrary shape to be constructed for
25//! any tile.
26
27use crate::{
28    asset::{Resource, ResourceData},
29    core::{
30        algebra::{Matrix4, Point2, Point3, Vector2},
31        reflect::prelude::*,
32        type_traits::prelude::*,
33        visitor::prelude::*,
34    },
35};
36use std::{
37    error::Error,
38    fmt::{Debug, Display, Formatter},
39    num::{ParseFloatError, ParseIntError},
40    path::Path,
41    str::FromStr,
42};
43use strum_macros::{AsRefStr, EnumString, VariantNames};
44
45use super::*;
46
47/// Supported collider types for tiles.
48#[derive(
49    Clone,
50    Hash,
51    PartialEq,
52    Eq,
53    Default,
54    Visit,
55    Reflect,
56    AsRefStr,
57    EnumString,
58    VariantNames,
59    TypeUuidProvider,
60)]
61#[type_uuid(id = "04a44fec-394f-4497-97d5-fe9e6f915831")]
62pub enum TileCollider {
63    /// No collider.
64    #[default]
65    None,
66    /// Rectangle collider that covers the full tile.
67    Rectangle,
68    /// User-defined collider containing a reference to a resource that contains the triangles.
69    Custom(CustomTileColliderResource),
70    /// Mesh collider, the mesh is autogenerated.
71    Mesh,
72}
73
74impl Default for &TileCollider {
75    fn default() -> Self {
76        &TileCollider::None
77    }
78}
79
80impl Debug for TileCollider {
81    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82        match self {
83            Self::None => write!(f, "None"),
84            Self::Rectangle => write!(f, "Rectangle"),
85            Self::Custom(r) => write!(f, "Custom({})", r.data_ref().deref()),
86            Self::Mesh => write!(f, "Mesh"),
87        }
88    }
89}
90
91impl OrthoTransform for TileCollider {
92    fn x_flipped(self) -> Self {
93        if let Self::Custom(collider) = self {
94            let collider = collider.data_ref().clone();
95            Self::Custom(Resource::new_ok(
96                ResourceKind::Embedded,
97                collider.x_flipped(),
98            ))
99        } else {
100            self
101        }
102    }
103    fn rotated(self, amount: i8) -> Self {
104        if let Self::Custom(collider) = self {
105            let collider = collider.data_ref().clone();
106            Self::Custom(Resource::new_ok(
107                ResourceKind::Embedded,
108                collider.rotated(amount),
109            ))
110        } else {
111            self
112        }
113    }
114}
115
116impl TileCollider {
117    /// This collider is empty.
118    pub fn is_none(&self) -> bool {
119        matches!(self, TileCollider::None)
120    }
121    /// This collider is a full rectangle.
122    pub fn is_rectangle(&self) -> bool {
123        matches!(self, TileCollider::Rectangle)
124    }
125    /// This collider is a custom mesh.
126    pub fn is_custom(&self) -> bool {
127        matches!(self, TileCollider::Custom(_))
128    }
129
130    /// Generate the mesh for this collider.
131    pub fn build_collider_shape(
132        &self,
133        transform: &Matrix4<f32>,
134        position: Vector3<f32>,
135        vertices: &mut Vec<Point2<f32>>,
136        triangles: &mut Vec<[u32; 3]>,
137    ) {
138        match self {
139            TileCollider::None => (),
140            TileCollider::Rectangle => {
141                let origin = vertices.len() as u32;
142                for (dx, dy) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] {
143                    let offset = Vector3::new(dx, dy, 0.0);
144                    let point = Point3::from(position + offset);
145                    vertices.push(transform.transform_point(&point).xy());
146                }
147
148                triangles.push([origin, origin + 1, origin + 2]);
149                triangles.push([origin, origin + 2, origin + 3]);
150            }
151            TileCollider::Custom(resource) => {
152                resource
153                    .data_ref()
154                    .build_collider_shape(transform, position, vertices, triangles);
155            }
156            TileCollider::Mesh => (), // TODO: Add image-to-mesh conversion
157        }
158    }
159}
160
161/// A resource to hold triangle data for a tile collider arranged in rectangle from (0,0) to (1,1).
162pub type CustomTileColliderResource = Resource<CustomTileCollider>;
163/// Triangle data for a tile collider arranged in rectangle from (0,0) to (1,1).
164/// Custom tile colliders can be converted to and from strings, where the strings contain
165/// 2D vertex positions and triangle index triples. A group of two numbers is taken to be
166/// a 2D vertex position while a group of three numbers is taken to be an index triple,
167/// and the numbers are therefore parsed as ints. For example, "0,0 1,1 1,0 0,1,2" would be
168/// a valid string for a custom tile collider. The commas (,) are used to connect two numbers
169/// as being part of the same group. Any other characters are ignored, so this would also be
170/// accepted: "(0,0) (1,1) (1,0) \[0,1,2\]".
171#[derive(Clone, PartialEq, Debug, Default, Visit, Reflect, TypeUuidProvider)]
172#[type_uuid(id = "118da556-a444-4bd9-bd88-12d78d26107f")]
173pub struct CustomTileCollider {
174    /// The vertices of the triangles, with the boundaries of the tile being between (0,0) and (1,1).
175    pub vertices: Vec<Vector2<f32>>,
176    /// The indices of the vertices of each triangle
177    pub triangles: Vec<TriangleDefinition>,
178}
179
180impl ResourceData for CustomTileCollider {
181    fn type_uuid(&self) -> Uuid {
182        <Self as TypeUuidProvider>::type_uuid()
183    }
184
185    fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
186        let mut visitor = Visitor::new();
187        self.visit("CustomTileCollider", &mut visitor)?;
188        visitor.save_binary(path)?;
189        Ok(())
190    }
191
192    fn can_be_saved(&self) -> bool {
193        false
194    }
195}
196
197impl OrthoTransform for CustomTileCollider {
198    fn x_flipped(self) -> Self {
199        Self {
200            vertices: self
201                .vertices
202                .iter()
203                .map(|v| Vector2::new(1.0 - v.x, v.y))
204                .collect(),
205            ..self
206        }
207    }
208
209    fn rotated(self, amount: i8) -> Self {
210        let center = Vector2::new(0.5, 0.5);
211        Self {
212            vertices: self
213                .vertices
214                .iter()
215                .map(|v| (v - center).rotated(amount) + center)
216                .collect(),
217            ..self
218        }
219    }
220}
221
222impl CustomTileCollider {
223    /// Construct triangles to represent this collider by appending to the given
224    /// vectors, starting from the given lower-left corner of the tile and finally
225    /// applying the given transformation matrix.
226    /// The transformation and position are in 3D, but the resulting 2D vertices
227    /// ignore the z-coordinate.
228    pub fn build_collider_shape(
229        &self,
230        transform: &Matrix4<f32>,
231        position: Vector3<f32>,
232        vertices: &mut Vec<Point2<f32>>,
233        triangles: &mut Vec<[u32; 3]>,
234    ) {
235        let origin = vertices.len() as u32;
236        triangles.extend(self.triangles.iter().map(|d| d.0.map(|i| i + origin)));
237        vertices.extend(self.vertices.iter().map(|p| {
238            transform
239                .transform_point(&Point3::from(position + p.to_homogeneous()))
240                .xy()
241        }));
242    }
243}
244
245impl Display for CustomTileCollider {
246    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
247        let mut first = true;
248        for v in self.vertices.iter() {
249            if !first {
250                write!(f, " ")?;
251            }
252            first = false;
253            write!(f, "({}, {})", v.x, v.y)?;
254        }
255        for TriangleDefinition(t) in self.triangles.iter() {
256            if !first {
257                write!(f, " ")?;
258            }
259            first = false;
260            write!(f, "[{}, {}, {}]", t[0], t[1], t[2])?;
261        }
262        Ok(())
263    }
264}
265
266/// Errors that may occur while parsing a custom tile collider.
267#[derive(Debug)]
268pub enum CustomTileColliderStrError {
269    /// A group is shorter than 2. For example: "7"
270    GroupTooShort,
271    /// A group is longer than 3. For example: "7,7,8,9"
272    GroupTooLong(usize),
273    /// A comma (,) was found without a number. For example: "7,"
274    MissingNumber,
275    /// A triangle does not match any of the given vertices.
276    /// For example: "0,0 1,1 0,1,2". The final "2" is illegal because there are only two vertices given.
277    IndexOutOfBounds(u32),
278    /// Failed to parse an entry in a length-2 group as an f32. For example: "0,0.2.3"
279    IndexParseError(ParseIntError),
280    /// Failed to parse an entry in a length-3 group as a u32. For example: "0,1.2,3"
281    CoordinateParseError(ParseFloatError),
282}
283
284impl From<ParseIntError> for CustomTileColliderStrError {
285    fn from(value: ParseIntError) -> Self {
286        Self::IndexParseError(value)
287    }
288}
289
290impl From<ParseFloatError> for CustomTileColliderStrError {
291    fn from(value: ParseFloatError) -> Self {
292        Self::CoordinateParseError(value)
293    }
294}
295
296impl Error for CustomTileColliderStrError {}
297
298impl Display for CustomTileColliderStrError {
299    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
300        match self {
301            CustomTileColliderStrError::GroupTooShort => {
302                write!(f, "Each group must have at least 2 numbers.")
303            }
304            CustomTileColliderStrError::GroupTooLong(n) => {
305                write!(f, "A group has {n} numbers. No group may be longer than 3.")
306            }
307            CustomTileColliderStrError::IndexOutOfBounds(n) => {
308                write!(
309                    f,
310                    "Triangle index {n} does not match any of the given vertices."
311                )
312            }
313            CustomTileColliderStrError::MissingNumber => {
314                write!(f, "Numbers in a group must be separated by commas.")
315            }
316            CustomTileColliderStrError::IndexParseError(parse_int_error) => {
317                write!(f, "Index parse failure: {parse_int_error}")
318            }
319            CustomTileColliderStrError::CoordinateParseError(parse_float_error) => {
320                write!(f, "Coordinate parse failure: {parse_float_error}")
321            }
322        }
323    }
324}
325
326impl FromStr for CustomTileCollider {
327    type Err = CustomTileColliderStrError;
328
329    fn from_str(s: &str) -> Result<Self, Self::Err> {
330        let mut group = Vec::<&str>::default();
331        let mut ready = true;
332        let mut vertices = Vec::<Vector2<f32>>::default();
333        let mut triangles = Vec::<TriangleDefinition>::default();
334        for token in TokenIter::new(s) {
335            if ready {
336                if token != "," {
337                    group.push(token);
338                    ready = false;
339                } else {
340                    return Err(CustomTileColliderStrError::MissingNumber);
341                }
342            } else if token != "," {
343                process_group(&group, &mut vertices, &mut triangles)?;
344                group.clear();
345                group.push(token);
346            } else {
347                ready = true;
348            }
349        }
350        if !group.is_empty() {
351            process_group(&group, &mut vertices, &mut triangles)?;
352        }
353        let len = vertices.len() as u32;
354        for TriangleDefinition(tri) in triangles.iter() {
355            for &n in tri.iter() {
356                if n >= len {
357                    return Err(CustomTileColliderStrError::IndexOutOfBounds(n));
358                }
359            }
360        }
361        Ok(Self {
362            vertices,
363            triangles,
364        })
365    }
366}
367
368fn process_group(
369    group: &[&str],
370    vertices: &mut Vec<Vector2<f32>>,
371    triangles: &mut Vec<TriangleDefinition>,
372) -> Result<(), CustomTileColliderStrError> {
373    use CustomTileColliderStrError as Error;
374    let len = group.len();
375    if len < 2 {
376        return Err(Error::GroupTooShort);
377    } else if len > 3 {
378        return Err(Error::GroupTooLong(group.len()));
379    } else if len == 2 {
380        let v = Vector2::new(parse_f32(group[0])?, parse_f32(group[1])?);
381        vertices.push(v);
382    } else if len == 3 {
383        let t = TriangleDefinition([
384            u32::from_str(group[0])?,
385            u32::from_str(group[1])?,
386            u32::from_str(group[2])?,
387        ]);
388        triangles.push(t);
389    }
390    Ok(())
391}
392
393fn parse_f32(source: &str) -> Result<f32, ParseFloatError> {
394    let value = f32::from_str(source)?;
395    f32::from_str(&format!("{value:.3}"))
396}
397
398struct TokenIter<'a> {
399    source: &'a str,
400    position: usize,
401}
402
403impl<'a> TokenIter<'a> {
404    fn new(source: &'a str) -> Self {
405        Self {
406            source,
407            position: 0,
408        }
409    }
410}
411
412fn is_number_char(c: char) -> bool {
413    c.is_numeric() || c == '.' || c == '-'
414}
415
416fn is_ignore_char(c: char) -> bool {
417    !is_number_char(c) && c != ','
418}
419
420impl<'a> Iterator for TokenIter<'a> {
421    type Item = &'a str;
422
423    fn next(&mut self) -> Option<Self::Item> {
424        let rest = self.source.get(self.position..)?;
425        if rest.is_empty() {
426            return None;
427        }
428        let mut initial_ignore = true;
429        let mut start = 0;
430        for (i, c) in rest.char_indices() {
431            if initial_ignore {
432                if is_ignore_char(c) {
433                    continue;
434                } else {
435                    initial_ignore = false;
436                    start = i;
437                }
438            }
439            if c == ',' {
440                if i == start {
441                    self.position += i + 1;
442                    return Some(&rest[start..i + 1]);
443                } else {
444                    self.position += i;
445                    return Some(&rest[start..i]);
446                }
447            } else if is_ignore_char(c) {
448                self.position += i + 1;
449                return Some(&rest[start..i]);
450            }
451        }
452        if initial_ignore {
453            return None;
454        }
455        self.position = self.source.len();
456        Some(&rest[start..])
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn empty() {
466        let mut iter = TokenIter::new("");
467        assert_eq!(iter.next(), None);
468    }
469    #[test]
470    fn empty2() {
471        let mut iter = TokenIter::new("   ");
472        assert_eq!(iter.next(), None);
473    }
474
475    #[test]
476    fn comma() {
477        let mut iter = TokenIter::new("0,1");
478        assert_eq!(iter.next().unwrap(), "0");
479        assert_eq!(iter.next().unwrap(), ",");
480        assert_eq!(iter.next().unwrap(), "1");
481        assert_eq!(iter.next(), None);
482    }
483    #[test]
484    fn comma2() {
485        let mut iter = TokenIter::new(" 0.4 , -1 ");
486        assert_eq!(iter.next().unwrap(), "0.4");
487        assert_eq!(iter.next().unwrap(), ",");
488        assert_eq!(iter.next().unwrap(), "-1");
489        assert_eq!(iter.next(), None);
490    }
491    #[test]
492    fn comma3() {
493        let mut iter = TokenIter::new(",, ,");
494        assert_eq!(iter.next().unwrap(), ",");
495        assert_eq!(iter.next().unwrap(), ",");
496        assert_eq!(iter.next().unwrap(), ",");
497        assert_eq!(iter.next(), None);
498    }
499    #[test]
500    fn number() {
501        let mut iter = TokenIter::new("0");
502        assert_eq!(iter.next().unwrap(), "0");
503        assert_eq!(iter.next(), None);
504    }
505    #[test]
506    fn number2() {
507        let mut iter = TokenIter::new("-3.14");
508        assert_eq!(iter.next().unwrap(), "-3.14");
509        assert_eq!(iter.next(), None);
510    }
511    #[test]
512    fn number3() {
513        let mut iter = TokenIter::new("  -3.14 ");
514        assert_eq!(iter.next().unwrap(), "-3.14");
515        assert_eq!(iter.next(), None);
516    }
517    #[test]
518    fn collider() {
519        let col = CustomTileCollider::from_str("0,0; 1,1; 1,0; 0,1,2").unwrap();
520        assert_eq!(col.vertices.len(), 3);
521        assert_eq!(col.vertices[0], Vector2::new(0.0, 0.0));
522        assert_eq!(col.vertices[1], Vector2::new(1.0, 1.0));
523        assert_eq!(col.vertices[2], Vector2::new(1.0, 0.0));
524        assert_eq!(col.triangles.len(), 1);
525        assert_eq!(col.triangles[0], TriangleDefinition([0, 1, 2]));
526    }
527    #[test]
528    fn collider_display() {
529        let col = CustomTileCollider::from_str("0,0; 1,1; 1,0.333; 0,1,2").unwrap();
530        assert_eq!(col.to_string(), "(0, 0) (1, 1) (1, 0.333) [0, 1, 2]");
531    }
532}