Skip to main content

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) => {
86                if r.is_ok() {
87                    write!(f, "Custom({})", r.data_ref().deref())
88                } else {
89                    f.write_str("Custom(unloaded)")
90                }
91            }
92            Self::Mesh => write!(f, "Mesh"),
93        }
94    }
95}
96
97impl OrthoTransform for TileCollider {
98    fn x_flipped(self) -> Self {
99        if let Self::Custom(collider) = self {
100            let collider = collider.data_ref().clone();
101            Self::Custom(Resource::new_ok(
102                Uuid::new_v4(),
103                ResourceKind::Embedded,
104                collider.x_flipped(),
105            ))
106        } else {
107            self
108        }
109    }
110    fn rotated(self, amount: i8) -> Self {
111        if let Self::Custom(collider) = self {
112            let collider = collider.data_ref().clone();
113            Self::Custom(Resource::new_ok(
114                Uuid::new_v4(),
115                ResourceKind::Embedded,
116                collider.rotated(amount),
117            ))
118        } else {
119            self
120        }
121    }
122}
123
124impl TileCollider {
125    /// This collider is empty.
126    pub fn is_none(&self) -> bool {
127        matches!(self, TileCollider::None)
128    }
129    /// This collider is a full rectangle.
130    pub fn is_rectangle(&self) -> bool {
131        matches!(self, TileCollider::Rectangle)
132    }
133    /// This collider is a custom mesh.
134    pub fn is_custom(&self) -> bool {
135        matches!(self, TileCollider::Custom(_))
136    }
137
138    /// Generate the mesh for this collider.
139    pub fn build_collider_shape(
140        &self,
141        transform: &Matrix4<f32>,
142        position: Vector3<f32>,
143        vertices: &mut Vec<Point2<f32>>,
144        triangles: &mut Vec<[u32; 3]>,
145    ) {
146        match self {
147            TileCollider::None => (),
148            TileCollider::Rectangle => {
149                let origin = vertices.len() as u32;
150                for (dx, dy) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] {
151                    let offset = Vector3::new(dx, dy, 0.0);
152                    let point = Point3::from(position + offset);
153                    vertices.push(transform.transform_point(&point).xy());
154                }
155
156                triangles.push([origin, origin + 1, origin + 2]);
157                triangles.push([origin, origin + 2, origin + 3]);
158            }
159            TileCollider::Custom(resource) => {
160                if resource.is_ok() {
161                    resource
162                        .data_ref()
163                        .build_collider_shape(transform, position, vertices, triangles);
164                }
165            }
166            TileCollider::Mesh => (), // TODO: Add image-to-mesh conversion
167        }
168    }
169}
170
171/// A resource to hold triangle data for a tile collider arranged in rectangle from (0,0) to (1,1).
172pub type CustomTileColliderResource = Resource<CustomTileCollider>;
173/// Triangle data for a tile collider arranged in rectangle from (0,0) to (1,1).
174/// Custom tile colliders can be converted to and from strings, where the strings contain
175/// 2D vertex positions and triangle index triples. A group of two numbers is taken to be
176/// a 2D vertex position while a group of three numbers is taken to be an index triple,
177/// and the numbers are therefore parsed as ints. For example, "0,0 1,1 1,0 0,1,2" would be
178/// a valid string for a custom tile collider. The commas (,) are used to connect two numbers
179/// as being part of the same group. Any other characters are ignored, so this would also be
180/// accepted: "(0,0) (1,1) (1,0) \[0,1,2\]".
181#[derive(Clone, PartialEq, Debug, Default, Visit, Reflect, TypeUuidProvider)]
182#[type_uuid(id = "118da556-a444-4bd9-bd88-12d78d26107f")]
183pub struct CustomTileCollider {
184    /// The vertices of the triangles, with the boundaries of the tile being between (0,0) and (1,1).
185    pub vertices: Vec<Vector2<f32>>,
186    /// The indices of the vertices of each triangle
187    pub triangles: Vec<TriangleDefinition>,
188}
189
190impl ResourceData for CustomTileCollider {
191    fn type_uuid(&self) -> Uuid {
192        <Self as TypeUuidProvider>::type_uuid()
193    }
194
195    fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
196        let mut visitor = Visitor::new();
197        self.visit("CustomTileCollider", &mut visitor)?;
198        visitor.save_ascii_to_file(path)?;
199        Ok(())
200    }
201
202    fn can_be_saved(&self) -> bool {
203        false
204    }
205
206    fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
207        Some(Box::new(self.clone()))
208    }
209}
210
211impl OrthoTransform for CustomTileCollider {
212    fn x_flipped(self) -> Self {
213        Self {
214            vertices: self
215                .vertices
216                .iter()
217                .map(|v| Vector2::new(1.0 - v.x, v.y))
218                .collect(),
219            ..self
220        }
221    }
222
223    fn rotated(self, amount: i8) -> Self {
224        let center = Vector2::new(0.5, 0.5);
225        Self {
226            vertices: self
227                .vertices
228                .iter()
229                .map(|v| (v - center).rotated(amount) + center)
230                .collect(),
231            ..self
232        }
233    }
234}
235
236impl CustomTileCollider {
237    /// Construct triangles to represent this collider by appending to the given
238    /// vectors, starting from the given lower-left corner of the tile and finally
239    /// applying the given transformation matrix.
240    /// The transformation and position are in 3D, but the resulting 2D vertices
241    /// ignore the z-coordinate.
242    pub fn build_collider_shape(
243        &self,
244        transform: &Matrix4<f32>,
245        position: Vector3<f32>,
246        vertices: &mut Vec<Point2<f32>>,
247        triangles: &mut Vec<[u32; 3]>,
248    ) {
249        let origin = vertices.len() as u32;
250        triangles.extend(self.triangles.iter().map(|d| d.0.map(|i| i + origin)));
251        vertices.extend(self.vertices.iter().map(|p| {
252            transform
253                .transform_point(&Point3::from(position + p.to_homogeneous()))
254                .xy()
255        }));
256    }
257}
258
259impl Display for CustomTileCollider {
260    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
261        let mut first = true;
262        for v in self.vertices.iter() {
263            if !first {
264                write!(f, " ")?;
265            }
266            first = false;
267            write!(f, "({}, {})", v.x, v.y)?;
268        }
269        for TriangleDefinition(t) in self.triangles.iter() {
270            if !first {
271                write!(f, " ")?;
272            }
273            first = false;
274            write!(f, "[{}, {}, {}]", t[0], t[1], t[2])?;
275        }
276        Ok(())
277    }
278}
279
280/// Errors that may occur while parsing a custom tile collider.
281#[derive(Debug)]
282pub enum CustomTileColliderStrError {
283    /// A group is shorter than 2. For example: "7"
284    GroupTooShort,
285    /// A group is longer than 3. For example: "7,7,8,9"
286    GroupTooLong(usize),
287    /// A comma (,) was found without a number. For example: "7,"
288    MissingNumber,
289    /// A triangle does not match any of the given vertices.
290    /// For example: "0,0 1,1 0,1,2". The final "2" is illegal because there are only two vertices given.
291    IndexOutOfBounds(u32),
292    /// Failed to parse an entry in a length-2 group as an f32. For example: "0,0.2.3"
293    IndexParseError(ParseIntError),
294    /// Failed to parse an entry in a length-3 group as a u32. For example: "0,1.2,3"
295    CoordinateParseError(ParseFloatError),
296}
297
298impl From<ParseIntError> for CustomTileColliderStrError {
299    fn from(value: ParseIntError) -> Self {
300        Self::IndexParseError(value)
301    }
302}
303
304impl From<ParseFloatError> for CustomTileColliderStrError {
305    fn from(value: ParseFloatError) -> Self {
306        Self::CoordinateParseError(value)
307    }
308}
309
310impl Error for CustomTileColliderStrError {}
311
312impl Display for CustomTileColliderStrError {
313    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
314        match self {
315            CustomTileColliderStrError::GroupTooShort => {
316                write!(f, "Each group must have at least 2 numbers.")
317            }
318            CustomTileColliderStrError::GroupTooLong(n) => {
319                write!(f, "A group has {n} numbers. No group may be longer than 3.")
320            }
321            CustomTileColliderStrError::IndexOutOfBounds(n) => {
322                write!(
323                    f,
324                    "Triangle index {n} does not match any of the given vertices."
325                )
326            }
327            CustomTileColliderStrError::MissingNumber => {
328                write!(f, "Numbers in a group must be separated by commas.")
329            }
330            CustomTileColliderStrError::IndexParseError(parse_int_error) => {
331                write!(f, "Index parse failure: {parse_int_error}")
332            }
333            CustomTileColliderStrError::CoordinateParseError(parse_float_error) => {
334                write!(f, "Coordinate parse failure: {parse_float_error}")
335            }
336        }
337    }
338}
339
340impl FromStr for CustomTileCollider {
341    type Err = CustomTileColliderStrError;
342
343    fn from_str(s: &str) -> Result<Self, Self::Err> {
344        let mut group = Vec::<&str>::default();
345        let mut ready = true;
346        let mut vertices = Vec::<Vector2<f32>>::default();
347        let mut triangles = Vec::<TriangleDefinition>::default();
348        for token in TokenIter::new(s) {
349            if ready {
350                if token != "," {
351                    group.push(token);
352                    ready = false;
353                } else {
354                    return Err(CustomTileColliderStrError::MissingNumber);
355                }
356            } else if token != "," {
357                process_group(&group, &mut vertices, &mut triangles)?;
358                group.clear();
359                group.push(token);
360            } else {
361                ready = true;
362            }
363        }
364        if !group.is_empty() {
365            process_group(&group, &mut vertices, &mut triangles)?;
366        }
367        let len = vertices.len() as u32;
368        for TriangleDefinition(tri) in triangles.iter() {
369            for &n in tri.iter() {
370                if n >= len {
371                    return Err(CustomTileColliderStrError::IndexOutOfBounds(n));
372                }
373            }
374        }
375        Ok(Self {
376            vertices,
377            triangles,
378        })
379    }
380}
381
382fn process_group(
383    group: &[&str],
384    vertices: &mut Vec<Vector2<f32>>,
385    triangles: &mut Vec<TriangleDefinition>,
386) -> Result<(), CustomTileColliderStrError> {
387    use CustomTileColliderStrError as Error;
388    let len = group.len();
389    if len < 2 {
390        return Err(Error::GroupTooShort);
391    } else if len > 3 {
392        return Err(Error::GroupTooLong(group.len()));
393    } else if len == 2 {
394        let v = Vector2::new(parse_f32(group[0])?, parse_f32(group[1])?);
395        vertices.push(v);
396    } else if len == 3 {
397        let t = TriangleDefinition([
398            u32::from_str(group[0])?,
399            u32::from_str(group[1])?,
400            u32::from_str(group[2])?,
401        ]);
402        triangles.push(t);
403    }
404    Ok(())
405}
406
407fn parse_f32(source: &str) -> Result<f32, ParseFloatError> {
408    let value = f32::from_str(source)?;
409    f32::from_str(&format!("{value:.3}"))
410}
411
412struct TokenIter<'a> {
413    source: &'a str,
414    position: usize,
415}
416
417impl<'a> TokenIter<'a> {
418    fn new(source: &'a str) -> Self {
419        Self {
420            source,
421            position: 0,
422        }
423    }
424}
425
426fn is_number_char(c: char) -> bool {
427    c.is_numeric() || c == '.' || c == '-'
428}
429
430fn is_ignore_char(c: char) -> bool {
431    !is_number_char(c) && c != ','
432}
433
434impl<'a> Iterator for TokenIter<'a> {
435    type Item = &'a str;
436
437    fn next(&mut self) -> Option<Self::Item> {
438        let rest = self.source.get(self.position..)?;
439        if rest.is_empty() {
440            return None;
441        }
442        let mut initial_ignore = true;
443        let mut start = 0;
444        for (i, c) in rest.char_indices() {
445            if initial_ignore {
446                if is_ignore_char(c) {
447                    continue;
448                } else {
449                    initial_ignore = false;
450                    start = i;
451                }
452            }
453            if c == ',' {
454                if i == start {
455                    self.position += i + 1;
456                    return Some(&rest[start..i + 1]);
457                } else {
458                    self.position += i;
459                    return Some(&rest[start..i]);
460                }
461            } else if is_ignore_char(c) {
462                self.position += i + 1;
463                return Some(&rest[start..i]);
464            }
465        }
466        if initial_ignore {
467            return None;
468        }
469        self.position = self.source.len();
470        Some(&rest[start..])
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn empty() {
480        let mut iter = TokenIter::new("");
481        assert_eq!(iter.next(), None);
482    }
483    #[test]
484    fn empty2() {
485        let mut iter = TokenIter::new("   ");
486        assert_eq!(iter.next(), None);
487    }
488
489    #[test]
490    fn comma() {
491        let mut iter = TokenIter::new("0,1");
492        assert_eq!(iter.next().unwrap(), "0");
493        assert_eq!(iter.next().unwrap(), ",");
494        assert_eq!(iter.next().unwrap(), "1");
495        assert_eq!(iter.next(), None);
496    }
497    #[test]
498    fn comma2() {
499        let mut iter = TokenIter::new(" 0.4 , -1 ");
500        assert_eq!(iter.next().unwrap(), "0.4");
501        assert_eq!(iter.next().unwrap(), ",");
502        assert_eq!(iter.next().unwrap(), "-1");
503        assert_eq!(iter.next(), None);
504    }
505    #[test]
506    fn comma3() {
507        let mut iter = TokenIter::new(",, ,");
508        assert_eq!(iter.next().unwrap(), ",");
509        assert_eq!(iter.next().unwrap(), ",");
510        assert_eq!(iter.next().unwrap(), ",");
511        assert_eq!(iter.next(), None);
512    }
513    #[test]
514    fn number() {
515        let mut iter = TokenIter::new("0");
516        assert_eq!(iter.next().unwrap(), "0");
517        assert_eq!(iter.next(), None);
518    }
519    #[test]
520    fn number2() {
521        let mut iter = TokenIter::new("-3.14");
522        assert_eq!(iter.next().unwrap(), "-3.14");
523        assert_eq!(iter.next(), None);
524    }
525    #[test]
526    fn number3() {
527        let mut iter = TokenIter::new("  -3.14 ");
528        assert_eq!(iter.next().unwrap(), "-3.14");
529        assert_eq!(iter.next(), None);
530    }
531    #[test]
532    fn collider() {
533        let col = CustomTileCollider::from_str("0,0; 1,1; 1,0; 0,1,2").unwrap();
534        assert_eq!(col.vertices.len(), 3);
535        assert_eq!(col.vertices[0], Vector2::new(0.0, 0.0));
536        assert_eq!(col.vertices[1], Vector2::new(1.0, 1.0));
537        assert_eq!(col.vertices[2], Vector2::new(1.0, 0.0));
538        assert_eq!(col.triangles.len(), 1);
539        assert_eq!(col.triangles[0], TriangleDefinition([0, 1, 2]));
540    }
541    #[test]
542    fn collider_display() {
543        let col = CustomTileCollider::from_str("0,0; 1,1; 1,0.333; 0,1,2").unwrap();
544        assert_eq!(col.to_string(), "(0, 0) (1, 1) (1, 0.333) [0, 1, 2]");
545    }
546}