Code/Tileset/TilesetComponent.cs
using Sandbox;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace SpriteTools;
[Category( "2D" )]
[Title( "2D Tileset" )]
[Icon( "calendar_view_month" )]
[Tint( EditorTint.Yellow )]
public partial class TilesetComponent : Component, Component.ExecuteInEditor
{
/// <summary>
/// The Layers within the TilesetComponent
/// </summary>
[Property, Group( "Layers" )]
public List<Layer> Layers
{
get => _layers;
set
{
_layers = value;
foreach ( var layer in _layers )
{
layer.TilesetComponent = this;
}
}
}
List<Layer> _layers;
[Property, WideMode( HasLabel = false )]
ComponentControls InternalControls { get; set; }
/// <summary>
/// Whether or not the component should generate a collider based on the specified Collision Layer
/// </summary>
[Property, FeatureEnabled( "Collision" )]
public bool HasCollider
{
get => _hasCollider;
set
{
if ( value == _hasCollider ) return;
_hasCollider = value;
if ( value ) CreateCollider();
else DestroyCollider();
}
}
bool _hasCollider;
/// <inheritdoc cref="Collider.Static" />
[Property, Feature( "Collision" )]
public bool Static
{
get => _static;
set
{
if ( value == _static ) return;
_static = value;
if ( Collider.IsValid() ) Collider.Static = value;
}
}
private bool _static = true;
/// <inheritdoc cref="Collider.IsTrigger" />
[Property, Feature( "Collision" )]
public bool IsTrigger
{
get => _isTrigger;
set
{
if ( value == _isTrigger ) return;
_isTrigger = value;
if ( Collider.IsValid() ) Collider.IsTrigger = value;
}
}
private bool _isTrigger = false;
/// <summary>
/// The width of the generated collider
/// </summary>
[Property, Feature( "Collision" )]
public float ColliderWidth
{
get => _colliderWidth;
set
{
if ( value < 0f ) _colliderWidth = 0f;
else if ( value == _colliderWidth ) return;
_colliderWidth = value;
Collider?.RebuildMesh();
}
}
float _colliderWidth;
/// <inheritdoc cref="Collider.Friction" />
[Property, Feature( "Collision" ), Group( "Surface Properties" )]
[Range( 0f, 1f, true, true ), Step( 0.01f )]
public float? Friction
{
get => _friction;
set
{
if ( value == _friction ) return;
_friction = value;
if ( Collider.IsValid() ) Collider.Friction = value;
}
}
private float? _friction;
/// <inheritdoc cref="Collider.Surface" />
[Property, Feature( "Collision" ), Group( "Surface Properties" )]
public Surface Surface
{
get => _surface;
set
{
if ( value == _surface ) return;
_surface = value;
if ( Collider.IsValid() ) Collider.Surface = value;
}
}
private Surface _surface;
/// <inheritdoc cref="Collider.SurfaceVelocity" />
[Property, Feature( "Collision" ), Group( "Surface Properties" )]
public Vector3 SurfaceVelocity
{
get => _surfaceVelocity;
set
{
if ( value == _surfaceVelocity ) return;
_surfaceVelocity = value;
if ( Collider.IsValid() ) Collider.SurfaceVelocity = value;
}
}
private Vector3 _surfaceVelocity;
[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
public Action<Collider> OnTriggerEnter { get; set; }
[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
public Action<Collider> OnTriggerExit { get; set; }
/// <summary>
/// Whether or not the associated Collider is dirty. Setting this to true will rebuild the Collider on the next frame.
/// </summary>
public bool IsDirty
{
get => Collider?.IsDirty ?? false;
set
{
if ( !Collider.IsValid() ) return;
Collider.IsDirty = value;
}
}
TilesetCollider Collider;
internal List<TilesetSceneObject> _sos = new();
protected override void OnEnabled ()
{
base.OnEnabled();
CreateCollider();
if ( Layers is null ) return;
foreach ( var layer in Layers )
{
layer.TilesetComponent = this;
}
}
protected override void OnDisabled ()
{
base.OnDisabled();
DestroyCollider();
foreach ( var _so in _sos )
{
_so.Delete();
}
_sos.Clear();
}
protected override void OnUpdate ()
{
base.OnUpdate();
_sos ??= new();
Layers ??= new();
var _newSos = new List<TilesetSceneObject>();
foreach ( var sos in _sos )
{
if ( sos is not null || sos.IsValid() )
{
_newSos.Add( sos );
}
else
{
sos?.Delete();
}
}
_sos = _newSos;
if ( Layers.Count != _sos.Count )
{
RebuildSceneObjects();
}
}
protected override void OnTagsChanged ()
{
base.OnTagsChanged();
foreach ( var _so in _sos )
_so?.Tags.SetFrom( Tags );
}
protected override void OnPreRender ()
{
base.OnPreRender();
if ( Layers is null ) return;
if ( Layers.Count == 0 )
{
return;
}
foreach ( var _so in _sos )
{
if ( !_so.IsValid() ) continue;
_so.RenderingEnabled = true;
_so.Transform = Transform.World;
_so.Flags.CastShadows = false;
_so.Flags.IsOpaque = false;
_so.Flags.IsTranslucent = true;
}
}
protected override void DrawGizmos ()
{
base.DrawGizmos();
var bounds = GetBounds();
Gizmo.Hitbox.BBox( bounds );
if ( !Gizmo.IsSelected ) return;
using ( Gizmo.Scope( "tileset", new Transform( 0, WorldRotation.Inverse, 1 ) ) )
{
Gizmo.Draw.Color = Color.Yellow;
Gizmo.Draw.LineThickness = 1f;
Gizmo.Draw.LineBBox( bounds );
}
}
public BBox GetBounds ()
{
var bounds = BBox.FromPositionAndSize( 0, 0 );
foreach ( var _so in _sos )
{
if ( !_so.IsValid() ) continue;
var boundSize = _so.Bounds.Size;
if ( ( boundSize.x + boundSize.y + boundSize.z ) > ( bounds.Size.x + bounds.Size.y + bounds.Size.z ) )
{
bounds = _so.Bounds.Translate( -_so.Position );
}
}
return bounds;
}
void RebuildSceneObjects ()
{
foreach ( var _so in _sos )
{
_so.Delete();
}
_sos = new List<TilesetSceneObject>();
for ( int i = 0; i < Layers.Count; i++ )
{
_sos.Add( new TilesetSceneObject( this, Scene.SceneWorld, i ) );
}
}
void CreateCollider ()
{
if ( !HasCollider ) return;
if ( Collider.IsValid() ) return;
Collider = AddComponent<TilesetCollider>();
Collider.Flags |= ComponentFlags.Hidden | ComponentFlags.NotSaved;
Collider.Tileset = this;
Collider.Static = Static;
Collider.IsTrigger = IsTrigger;
Collider.Friction = Friction;
Collider.Surface = Surface;
Collider.SurfaceVelocity = SurfaceVelocity;
Collider.OnTriggerEnter += OnTriggerEnter;
Collider.OnTriggerExit += OnTriggerExit;
}
void DestroyCollider ()
{
if ( Collider.IsValid() )
Collider.Destroy();
Collider = null;
}
/// <summary>
/// Returns the Layer with the specified name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public Layer GetLayerFromName ( string name )
{
return Layers.FirstOrDefault( x => x.Name == name );
}
/// <summary>
/// Returns the Layer at the specified index
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Layer GetLayerFromIndex ( int index )
{
if ( index < 0 || index >= Layers.Count ) return null;
return Layers[index];
}
public class Layer
{
/// <summary>
/// The name of the Layer
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether or not this Layer is currently being rendered
/// </summary>
public bool IsVisible { get; set; }
/// <summary>
/// Whether or not this Layer is locked. Locked Layers will ignore any attempted changes
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// The Tileset that this Layer uses
/// </summary>
[Property, Group( "Selected Layer" )] public TilesetResource TilesetResource { get; set; }
/// <summary>
/// The height of the Layer
/// </summary>
[Property, Group( "Selected Layer" )] public float? Height { get; set; } = null;
/// <summary>
/// Whether or not this Layer dictates the collision mesh
/// </summary>
[Group( "Selected Layer" ), Title( "Has Collisions" )] public bool IsCollisionLayer { get; set; }
/// <summary>
/// A dictionary of all Tiles in the layer by their position.
/// </summary>
public Dictionary<Vector2Int, Tile> Tiles { get; set; }
/// <summary>
/// A dictionary containing a list of positions for each Autotile Brush by their ID.
/// </summary>
public Dictionary<Guid, List<AutotilePosition>> Autotiles { get; set; }
/// <summary>
/// The TilesetComponent that this Layer belongs to
/// </summary>
[JsonIgnore, Hide] public TilesetComponent TilesetComponent { get; set; }
public Layer ( string name = "Untitled Layer" )
{
Name = name;
IsVisible = true;
IsLocked = false;
Tiles = new();
}
/// <summary>
/// Returns an exact copy of the Layer
/// </summary>
/// <returns></returns>
public Layer Copy ()
{
var layer = new Layer( Name )
{
IsVisible = IsVisible,
IsLocked = IsLocked,
Tiles = new(),
IsCollisionLayer = false,
TilesetComponent = TilesetComponent,
};
foreach ( var tile in Tiles )
{
layer.Tiles[tile.Key] = tile.Value.Copy();
}
return layer;
}
/// <summary>
/// Set a tile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="position"></param>
/// <param name="tileId"></param>
/// <param name="cellPosition"></param>
/// <param name="angle"></param>
/// <param name="flipX"></param>
/// <param name="flipY"></param>
/// <param name="rebuild"></param>
public void SetTile ( Vector2Int position, Guid tileId, Vector2Int cellPosition = default, int angle = 0, bool flipX = false, bool flipY = false, bool rebuild = true, bool removeAutotile = true )
{
if ( IsLocked ) return;
var tile = new Tile( tileId, cellPosition, angle, flipX, flipY );
Tiles[position] = tile;
if ( rebuild && TilesetComponent.IsValid() )
TilesetComponent.IsDirty = true;
if ( removeAutotile && Autotiles is not null )
{
foreach ( var group in Autotiles )
{
foreach ( var autotile in group.Value )
{
if ( autotile.Position == position )
{
Autotiles[group.Key].Remove( autotile );
break;
}
}
}
}
}
/// <summary>
/// Get the Tile at the specified position
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
public Tile GetTile ( Vector2Int position )
{
return Tiles[position];
}
/// <summary>
/// Get the Tile at the specified position
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
public Tile GetTile ( Vector3 position )
{
return Tiles[new Vector2Int( (int)position.x, (int)position.y )];
}
/// <summary>
/// Remove the Tile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="position"></param>
public void RemoveTile ( Vector2Int position )
{
if ( IsLocked ) return;
Tiles.Remove( position );
if ( Autotiles is not null )
{
foreach ( var group in Autotiles )
{
foreach ( var autotile in group.Value )
{
if ( autotile.Position == position )
{
Autotiles[group.Key].Remove( autotile );
break;
}
}
}
}
}
/// <summary>
/// Set an Autotile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="autotileBrush"></param>
/// <param name="position"></param>
/// <param name="enabled"></param>
/// <param name="update"></param>
/// <param name="isMerging"></param>
public void SetAutotile ( AutotileBrush autotileBrush, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
{
SetAutotile( autotileBrush.Id, position, enabled, update, isMerging );
}
/// <summary>
/// Set an Autotile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="autotileId"></param>
/// <param name="position"></param>
/// <param name="enabled"></param>
/// <param name="update"></param>
/// <param name="isMerging"></param>
public void SetAutotile ( Guid autotileId, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
{
if ( IsLocked ) return;
Autotiles ??= new();
foreach ( var group in Autotiles )
{
if ( group.Key == autotileId ) continue;
foreach ( var autotile in group.Value )
{
if ( autotile.Position == position )
{
Autotiles[group.Key].Remove( autotile );
break;
}
}
}
if ( !Autotiles.ContainsKey( autotileId ) )
Autotiles[autotileId] = new List<AutotilePosition>();
bool shouldUpdate = false;
if ( enabled )
{
if ( !Autotiles[autotileId].Any( x => x.Position == position ) )
{
Autotiles[autotileId].Add( new( position, isMerging ) );
shouldUpdate = true;
}
}
else
{
var foundPos = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
if ( foundPos is not null )
{
Tiles.Remove( position );
Autotiles[autotileId].Remove( foundPos );
shouldUpdate = true;
}
else
{
RemoveTile( position );
}
}
if ( update && shouldUpdate )
{
UpdateAutotile( autotileId, position, !enabled, shouldMerge: isMerging );
}
}
/// <summary>
/// Update the Autotile at the specified position. Used when manually modifying the placed autotiles.
/// </summary>
/// <param name="autotileId"></param>
/// <param name="position"></param>
/// <param name="checkErased"></param>
/// <param name="updateSurrounding"></param>
/// <param name="shouldMerge"></param>
public void UpdateAutotile ( Guid autotileId, Vector2Int position, bool checkErased, bool updateSurrounding = true, bool shouldMerge = false )
{
if ( !Autotiles.ContainsKey( autotileId ) ) return;
var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
var autotile = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
if ( autotile is not null )
{
if ( shouldMerge ) autotile.ShouldMerge = true;
if ( autotile.ShouldMerge ) shouldMerge = true;
var bitmask = GetAutotileBitmask( autotileId, position, shouldMerge );
if ( bitmask == -1 )
{
if ( checkErased ) RemoveTile( position );
}
else
{
if ( brush is not null )
{
var tile = brush.GetTileFromBitmask( bitmask );
if ( tile is not null )
{
SetTile( position, tile.Id, Vector2Int.Zero, 0, false, false, false, removeAutotile: false );
}
else
{
Log.Warning( $"Tile not found for bitmask {bitmask} in AutotileBrush {brush.Name}" );
}
}
}
}
if ( updateSurrounding )
{
var up = position.WithY( position.y + 1 );
var down = position.WithY( position.y - 1 );
var left = position.WithX( position.x - 1 );
var right = position.WithX( position.x + 1 );
var upLeft = up.WithX( left.x );
var upRight = up.WithX( right.x );
var downLeft = down.WithX( left.x );
var downRight = down.WithX( right.x );
if ( brush is not null && brush.AutotileType == AutotileType.Bitmask2x2Edge )
{
ClearInvalidAutotile( autotileId, up );
ClearInvalidAutotile( autotileId, down );
ClearInvalidAutotile( autotileId, left );
ClearInvalidAutotile( autotileId, right );
ClearInvalidAutotile( autotileId, upLeft );
ClearInvalidAutotile( autotileId, upRight );
ClearInvalidAutotile( autotileId, downLeft );
ClearInvalidAutotile( autotileId, downRight );
}
UpdateAutotile( autotileId, up, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, down, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, left, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, right, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, upLeft, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, upRight, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, downLeft, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, downRight, checkErased, false, shouldMerge );
}
}
void ClearInvalidAutotile ( Guid autotileId, Vector2Int position )
{
if ( !Tiles.TryGetValue( position, out var tile ) ) return;
var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
if ( brush is null ) return;
if ( brush.AutotileType != AutotileType.Bitmask2x2Edge ) return;
if ( !brush.Tiles.Any( x => x.Tiles.Any( y => y.Id == tile.TileId ) ) ) return;
if ( GetAutotileBitmask( autotileId, position ) != -1 ) return;
RemoveTile( position );
}
public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, bool mergeAll = false )
{
if ( Autotiles is null || ( !mergeAll && !Autotiles.ContainsKey( autotileId ) ) ) return -1;
List<AutotilePosition> positions = new();
if ( mergeAll )
{
foreach ( var kvp in Autotiles )
{
positions.AddRange( kvp.Value );
}
}
else
{
positions = Autotiles[autotileId];
}
int value = 0;
var up = position.WithY( position.y + 1 );
var down = position.WithY( position.y - 1 );
var left = position.WithX( position.x - 1 );
var right = position.WithX( position.x + 1 );
var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
if ( brush is null ) return 0;
bool is2x2 = brush.AutotileType == AutotileType.Bitmask2x2Edge;
if ( is2x2 )
{
foreach ( var pos in positions )
{
if ( pos.Position == up ) value += 1;
if ( pos.Position == left ) value += 2;
if ( pos.Position == right ) value += 4;
if ( pos.Position == down ) value += 8;
}
switch ( value )
{
case 0:
case 1:
case 2:
case 4:
case 8:
case 9:
case 6:
return -1;
}
value = 0;
}
var upLeft = up.WithX( left.x );
var upRight = up.WithX( right.x );
var downLeft = down.WithX( left.x );
var downRight = down.WithX( right.x );
foreach ( var thing in positions )
{
var pos = thing.Position;
if ( pos == upLeft ) value += 1;
if ( pos == up ) value += 2;
if ( pos == upRight ) value += 4;
if ( pos == left ) value += 8;
if ( pos == right ) value += 16;
if ( pos == downLeft ) value += 32;
if ( pos == down ) value += 64;
if ( pos == downRight ) value += 128;
}
if ( is2x2 )
{
switch ( value )
{
case 46:
case 116:
case 147:
case 201:
return -1;
}
}
return value;
}
public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, Dictionary<Vector2Int, bool> overrides, bool mergeAll = false )
{
if ( Autotiles is null ) return -1;
var positions = new List<Vector2Int>();
foreach ( var thing in Autotiles )
{
if ( !mergeAll && thing.Key != autotileId ) continue;
foreach ( var pos in thing.Value )
{
if ( !positions.Contains( pos.Position ) )
positions.Add( pos.Position );
}
}
int value = 0;
foreach ( var ride in overrides )
{
if ( ride.Value )
{
if ( !positions.Contains( ride.Key ) )
{
positions.Add( ride.Key );
}
}
else
{
if ( positions.Contains( ride.Key ) )
{
positions.Remove( ride.Key );
}
}
}
var up = position.WithY( position.y + 1 );
var down = position.WithY( position.y - 1 );
var left = position.WithX( position.x - 1 );
var right = position.WithX( position.x + 1 );
var upLeft = up.WithX( left.x );
var upRight = up.WithX( right.x );
var downLeft = down.WithX( left.x );
var downRight = down.WithX( right.x );
foreach ( var pos in positions )
{
if ( pos == upLeft ) value += 1;
if ( pos == up ) value += 2;
if ( pos == upRight ) value += 4;
if ( pos == left ) value += 8;
if ( pos == right ) value += 16;
if ( pos == downLeft ) value += 32;
if ( pos == down ) value += 64;
if ( pos == downRight ) value += 128;
}
return value;
}
public class AutotilePosition
{
public Vector2Int Position { get; set; }
public bool ShouldMerge { get; set; } = false;
public AutotilePosition ( Vector2Int position, bool shouldMerge = false )
{
Position = position;
ShouldMerge = shouldMerge;
}
}
}
public class Tile
{
public Guid TileId { get; set; } = Guid.NewGuid();
public Vector2Int CellPosition { get; set; }
public bool HorizontalFlip { get; set; }
public bool VerticalFlip { get; set; }
public int Rotation { get; set; }
public Vector2Int BakedPosition { get; set; }
public Tile () { }
public Tile ( Guid tileId, Vector2Int cellPosition, int rotation, bool flipX, bool flipY )
{
TileId = tileId;
CellPosition = cellPosition;
HorizontalFlip = flipX;
VerticalFlip = flipY;
Rotation = rotation;
}
public Tile Copy ()
{
return new Tile( TileId, CellPosition, Rotation, HorizontalFlip, VerticalFlip );
}
}
public class ComponentControls { }
}
internal sealed class TilesetSceneObject : SceneCustomObject
{
TilesetComponent Component;
Dictionary<TilesetResource, (TileAtlas, Material)> Materials = new();
Material MissingMaterial;
int LayerIndex;
public TilesetSceneObject ( TilesetComponent component, SceneWorld world, int layerIndex ) : base( world )
{
Component = component;
LayerIndex = layerIndex;
MissingMaterial = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
MissingMaterial.Set( "Texture", Texture.Load( "images/missing-tile.png" ) );
Tags.SetFrom( Component.Tags );
}
public override void RenderSceneObject ()
{
if ( Component?.Layers is null ) return;
var Layer = Component.Layers.ElementAtOrDefault( LayerIndex );
if ( Layer is null )
{
return;
}
var layers = Component.Layers.ToList();
layers.Reverse();
if ( layers.Count == 0 ) return;
Dictionary<Vector2Int, TilesetComponent.Tile> missingTiles = new();
if ( Layer?.IsVisible != true ) return;
int i = 0;
int layerIndex = layers.IndexOf( Layer );
{
var tileset = Layer.TilesetResource;
if ( tileset is null ) return;
var tilemap = tileset.TileMap;
var combo = GetMaterial( tileset );
if ( combo.Item1 is null || combo.Item2 is null ) return;
var tiling = combo.Item1.GetTiling();
var totalTiles = Layer.Tiles.Where( x => x.Value.TileId == default || tilemap.ContainsKey( x.Value.TileId ) );
var vertex = ArrayPool<Vertex>.Shared.Rent( totalTiles.Count() * 6 );
var minPosition = new Vector3( int.MaxValue, int.MaxValue, int.MaxValue );
var maxPosition = new Vector3( int.MinValue, int.MinValue, int.MinValue );
foreach ( var tile in Layer.Tiles )
{
var pos = tile.Key;
Vector2Int offsetPos = Vector2Int.Zero;
if ( tile.Value.TileId == default ) offsetPos = tile.Value.BakedPosition;
else
{
if ( !tilemap.ContainsKey( tile.Value.TileId ) )
{
missingTiles[pos] = tile.Value;
continue;
}
offsetPos = tilemap[tile.Value.TileId].Position;
}
var offset = combo.Item1.GetOffset( offsetPos + tile.Value.CellPosition );
if ( tile.Value.HorizontalFlip )
offset.x = -offset.x - tiling.x;
if ( !tile.Value.VerticalFlip )
offset.y = -offset.y - tiling.y;
var size = tileset.GetTileSize();
var position = new Vector3( pos.x, pos.y, Layer.Height ?? ( Component.Layers.Count - Component.Layers.IndexOf( Layer ) ) ) * new Vector3( size.x, size.y, 1 );
minPosition = Vector3.Min( minPosition, position );
maxPosition = Vector3.Max( maxPosition, position );
var topLeft = new Vector3( position.x, position.y, position.z );
var topRight = new Vector3( position.x + size.x, position.y, position.z );
var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );
var uvTopLeft = new Vector2( offset.x, offset.y );
var uvTopRight = new Vector2( offset.x + tiling.x, offset.y );
var uvBottomRight = new Vector2( offset.x + tiling.x, offset.y + tiling.y );
var uvBottomLeft = new Vector2( offset.x, offset.y + tiling.y );
if ( tile.Value.Rotation == 90 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvBottomLeft;
uvBottomLeft = uvBottomRight;
uvBottomRight = uvTopRight;
uvTopRight = tempUv;
}
else if ( tile.Value.Rotation == 180 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvBottomRight;
uvBottomRight = tempUv;
tempUv = uvTopRight;
uvTopRight = uvBottomLeft;
uvBottomLeft = tempUv;
}
else if ( tile.Value.Rotation == 270 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvTopRight;
uvTopRight = uvBottomRight;
uvBottomRight = uvBottomLeft;
uvBottomLeft = tempUv;
}
vertex[i] = new Vertex( topLeft );
vertex[i].TexCoord0 = uvTopLeft;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( topRight );
vertex[i].TexCoord0 = uvTopRight;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomRight );
vertex[i].TexCoord0 = uvBottomRight;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( topLeft );
vertex[i].TexCoord0 = uvTopLeft;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomRight );
vertex[i].TexCoord0 = uvBottomRight;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomLeft );
vertex[i].TexCoord0 = uvBottomLeft;
vertex[i].Normal = Vector3.Up;
i++;
}
Graphics.Draw( vertex, totalTiles.Count() * 6, combo.Item2, Attributes );
ArrayPool<Vertex>.Shared.Return( vertex );
var siz = tileset.GetTileSize();
maxPosition += new Vector3( siz.x, siz.y, 0 );
Bounds = new BBox( minPosition, maxPosition + Vector3.Down * 0.01f ).Rotate( Rotation ).Translate( Position );
}
if ( missingTiles.Count > 0 )
{
var uvTopLeft = new Vector2( 0, 0 );
var uvTopRight = new Vector2( 1, 0 );
var uvBottomRight = new Vector2( 1, 1 );
var uvBottomLeft = new Vector2( 0, 1 );
foreach ( var tile in missingTiles )
{
var material = MissingMaterial;
var pos = tile.Key;
var size = Component.Layers[0].TilesetResource.TileSize;
var position = new Vector3( pos.x, pos.y, 0 ) * new Vector3( size.x, size.y, 1 );
var topLeft = new Vector3( position.x, position.y, position.z );
var topRight = new Vector3( position.x + size.x, position.y, position.z );
var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );
var vertex = new Vertex[]
{
new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
new Vertex(topRight) { TexCoord0 = uvTopRight, Normal = Vector3.Up },
new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
new Vertex(bottomLeft) { TexCoord0 = uvBottomLeft, Normal = Vector3.Up },
};
Graphics.Draw( vertex, 6, material, Attributes );
}
}
}
(TileAtlas, Material) GetMaterial ( TilesetResource resource )
{
var texture = TileAtlas.FromTileset( resource );
if ( Materials.TryGetValue( resource, out var combo ) )
{
combo.Item1 = texture;
combo.Item2.Set( "Texture", texture );
}
else
{
var material = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
material.Set( "Texture", texture );
combo.Item1 = texture;
combo.Item2 = material;
Materials.Add( resource, combo );
}
return combo;
}
}