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