Skip to content

Commit 502a25f

Browse files
committed
Add TileMapView
1 parent 6fcbb3a commit 502a25f

File tree

1 file changed

+345
-0
lines changed

1 file changed

+345
-0
lines changed
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
/*
2+
* Copyright © 2025 Dustin Collins (Strega's Gate)
3+
* All Rights Reserved.
4+
*
5+
* http://stregasgate.com
6+
*/
7+
8+
open class TileMapView: View {
9+
public typealias SampleFilter = Material.Channel.SampleFilter
10+
11+
internal var material = Material()
12+
public var sampleFilter: SampleFilter {
13+
get {
14+
return material.channel(0) { channel in
15+
return channel.sampleFilter
16+
}
17+
}
18+
set {
19+
material.channel(0) { channel in
20+
channel.sampleFilter = newValue
21+
}
22+
}
23+
}
24+
25+
internal var needsSetup: Bool = true
26+
27+
public var isReady: Bool {
28+
return self.needsSetup == false && self.tileSet.isReady && self.tileMap.isReady
29+
}
30+
31+
public var tileSet: TileSet! = nil {
32+
didSet {
33+
needsSetup = true
34+
}
35+
}
36+
public var tileMap: TileMap! = nil {
37+
didSet {
38+
needsSetup = true
39+
}
40+
}
41+
42+
public var layers: [Layer] = []
43+
44+
public func editLayer(named name: String, _ block: (inout Layer)->()) {
45+
let index = self.layers.firstIndex(where: {$0.name == name})!
46+
var layer = self.layers[index]
47+
block(&layer)
48+
self.layers[index] = layer
49+
}
50+
51+
public init(tileSetPath: String, tileMapPath: String, sampleFilter: SampleFilter = .nearest) {
52+
self.tileSet = TileSet(path: tileSetPath)
53+
self.tileMap = TileMap(path: tileMapPath)
54+
super.init()
55+
self.sampleFilter = sampleFilter
56+
}
57+
58+
open override func update(withTimePassed deltaTime: Float) {
59+
super.update(withTimePassed: deltaTime)
60+
61+
if needsSetup, self.tileSet.isReady, self.tileMap.isReady {
62+
self.layers = tileMap.layers.map({Layer(layer: $0)})
63+
64+
self.material.channel(0) { channel in
65+
channel.texture = self.tileSet.texture
66+
}
67+
self.needsSetup = false
68+
self.setNeedsLayout()
69+
self.setNeedsUpdateConstraints()
70+
}else{
71+
self.updateAnimations(deltaTime: deltaTime)
72+
self.rebuild()
73+
}
74+
}
75+
76+
@MainActor public struct Layer {
77+
public let name: String?
78+
public let size: Size2
79+
public let tileSize: Size2
80+
public private(set) var tiles: [[TileMap.Tile]]
81+
public var animations: [TileAnimation] = []
82+
public private(set) var geometry: MutableGeometry = MutableGeometry()
83+
internal var needsRebuild: Bool = true
84+
85+
public var rows: Int {
86+
return tiles.count
87+
}
88+
public var columns: Int {
89+
return tiles.first?.count ?? 0
90+
}
91+
92+
public mutating func setTile(_ tile: TileMap.Tile, at coordinate: TileMap.Layer.Coordinate) {
93+
assert(containsCoordinate(coordinate), "Coordinate out of range")
94+
if self.tiles[coordinate.row][coordinate.column] != tile {
95+
self.tiles[coordinate.row][coordinate.column] = tile
96+
self.needsRebuild = true
97+
}
98+
}
99+
100+
public func containsCoordinate(_ coordinate: TileMap.Layer.Coordinate) -> Bool {
101+
return tiles.indices.contains(coordinate.row)
102+
&& tiles[coordinate.row].indices.contains(coordinate.column)
103+
}
104+
105+
public func coordinate(at position: Position2) -> TileMap.Layer.Coordinate? {
106+
let row = Int(position.y / tileSize.height)
107+
let column = Int(position.x / tileSize.width)
108+
if tiles.indices.contains(row) && tiles[row].indices.contains(column) {
109+
return TileMap.Layer.Coordinate(column: column, row: row)
110+
}
111+
return nil
112+
}
113+
114+
public func tileAtCoordinate(_ coordinate: TileMap.Layer.Coordinate) -> TileMap.Tile {
115+
assert(containsCoordinate(coordinate), "Coordinate out of range")
116+
return tiles[coordinate.row][coordinate.column]
117+
}
118+
119+
public func tileAtPosition(_ position: Position2) -> TileMap.Tile? {
120+
guard let coordinate = coordinate(at: position) else {return nil}
121+
return tileAtCoordinate(coordinate)
122+
}
123+
124+
public func rectForTileAt(_ coordinate: TileMap.Layer.Coordinate) -> Rect {
125+
assert(containsCoordinate(coordinate), "Coordinate out of range")
126+
let x = Float(coordinate.column)
127+
let y = Float(coordinate.row)
128+
let position = Position2(x, y) * tileSize
129+
return Rect(position: position, size: tileSize)
130+
}
131+
132+
// public func tileIndexAtCoordinate(column: Int, row: Int) -> Int {
133+
// return tiles[row][column].id
134+
// }
135+
//
136+
// public func tileIndexAtPosition(_ position: Position2) -> Int {
137+
// let column = position.x / tileSize.width
138+
// let row = position.y / tileSize.height
139+
// return tileIndexAtCoordinate(column: Int(column), row: Int(row))
140+
// }
141+
//
142+
// public func pixelCenterForTileAt(column: Int, row: Int) -> Position2 {
143+
// return (Position2(Float(column), Float(row)) * tileSize)
144+
// }
145+
146+
internal init(layer: TileMap.Layer) {
147+
self.name = layer.name
148+
self.size = layer.size
149+
self.tileSize = layer.tileSize
150+
self.tiles = layer.tiles
151+
}
152+
153+
public struct TileAnimation {
154+
let coordinate: TileMap.Layer.Coordinate
155+
let frames: [TileMap.Tile]
156+
let duration: Float
157+
var accumulatedTime: Float = 0
158+
let timePerFrame: Float
159+
var repeats: Bool
160+
161+
var previousTileIndex: Int = -1
162+
163+
private mutating func append(deltaTime: Float) {
164+
accumulatedTime += deltaTime
165+
if repeats {
166+
while accumulatedTime > duration {
167+
accumulatedTime -= duration
168+
}
169+
}else if accumulatedTime > duration {
170+
accumulatedTime = duration
171+
}
172+
}
173+
internal mutating func getNewTile(advancingBy deltaTime: Float) -> TileMap.Tile? {
174+
self.append(deltaTime: deltaTime)
175+
176+
let index = Int(accumulatedTime / timePerFrame)
177+
if previousTileIndex != index {
178+
self.previousTileIndex = index
179+
return frames[index]
180+
}
181+
return nil
182+
}
183+
184+
public init(coordinate: TileMap.Layer.Coordinate, frames: [TileMap.Tile], duration: Float, repeats: Bool = true) {
185+
self.coordinate = coordinate
186+
self.frames = frames
187+
if duration == 0 {
188+
self.duration = .ulpOfOne
189+
}else{
190+
self.duration = duration
191+
}
192+
self.timePerFrame = duration / Float(frames.count)
193+
self.repeats = repeats
194+
}
195+
}
196+
}
197+
198+
func updateAnimations(deltaTime: Float) {
199+
for layerIndex in layers.indices {
200+
for animationIndex in layers[layerIndex].animations.indices {
201+
if let tile = layers[layerIndex].animations[animationIndex].getNewTile(advancingBy: deltaTime) {
202+
let coordinate = layers[layerIndex].animations[animationIndex].coordinate
203+
layers[layerIndex].setTile(tile, at: coordinate)
204+
}
205+
}
206+
}
207+
}
208+
209+
func rebuild() {
210+
guard let tileSet else { return }
211+
guard let tileMap else { return }
212+
213+
for layerIndex in layers.indices {
214+
let layer = layers[layerIndex]
215+
guard layer.needsRebuild else { continue }
216+
217+
layers[layerIndex].needsRebuild = false
218+
219+
var triangles: [Triangle] = []
220+
triangles.reserveCapacity(Int(layer.size.width * layer.size.height) * 2)
221+
222+
let tileSize = tileSet.tileSize
223+
224+
let wM: Float = 1 / tileSet.texture.size.width
225+
let hM: Float = 1 / tileSet.texture.size.height
226+
for hIndex in 0 ..< Int(tileMap.size.height) {
227+
for wIndex in 0 ..< Int(tileMap.size.width) {
228+
let tile = layer.tileAtCoordinate(TileMap.Layer.Coordinate(column: wIndex, row: hIndex))
229+
guard tile.id > -1 else {continue}
230+
let tileRect = tileSet.rectForTile(tile)
231+
let position = Position2(
232+
x: Float(wIndex) * tileSize.width,
233+
y: Float(hIndex) * tileSize.height
234+
)
235+
let rect = Rect(position: position, size: tileSize)
236+
var v1 = Vertex(
237+
px: rect.x,
238+
py: rect.y,
239+
pz: 0,
240+
tu1: tileRect.x * wM,
241+
tv1: tileRect.y * hM
242+
)
243+
var v2 = Vertex(
244+
px: rect.maxX,
245+
py: rect.y,
246+
pz: 0,
247+
tu1: tileRect.maxX * wM,
248+
tv1: tileRect.y * hM
249+
)
250+
var v3 = Vertex(
251+
px: rect.maxX,
252+
py: rect.maxY,
253+
pz: 0,
254+
tu1: tileRect.maxX * wM,
255+
tv1: tileRect.maxY * hM
256+
)
257+
var v4 = Vertex(
258+
px: rect.x,
259+
py: rect.maxY,
260+
pz: 0,
261+
tu1: tileRect.x * wM,
262+
tv1: tileRect.maxY * hM
263+
)
264+
265+
if tile.options.contains(.flippedHorizontal) {
266+
swap(&v1.u1, &v2.u1)
267+
swap(&v3.u1, &v4.u1)
268+
}
269+
if tile.options.contains(.flippedVertical) {
270+
swap(&v1.v1, &v3.v1)
271+
swap(&v2.v1, &v4.v1)
272+
}
273+
if tile.options.contains(.flippedDiagonal) {
274+
swap(&v1.u1, &v3.u1)
275+
swap(&v1.v1, &v3.v1)
276+
}
277+
278+
triangles.append(Triangle(v1: v1, v2: v3, v3: v2, repairIfNeeded: false))
279+
triangles.append(Triangle(v1: v3, v2: v1, v3: v4, repairIfNeeded: false))
280+
}
281+
}
282+
if triangles.isEmpty == false {
283+
layer.geometry.rawGeometry = RawGeometry(triangles: triangles)
284+
}
285+
}
286+
}
287+
288+
public override func contentSize() -> Size2 {
289+
if let layer0 = self.layers.first {
290+
return layer0.size * layer0.tileSize
291+
}
292+
return super.contentSize()
293+
}
294+
295+
override func draw(_ rect: Rect, into canvas: inout UICanvas) {
296+
super.draw(rect, into: &canvas)
297+
298+
for layer in layers {
299+
// Calculate scale to fill rect
300+
let layerPointSize = layer.size * layer.tileSize
301+
let layerScale = rect.size / layerPointSize
302+
let layerScaledSize = layerPointSize * layerScale
303+
304+
if layerScaledSize == rect.size {
305+
canvas.insert(
306+
DrawCommand(
307+
resource: .geometry(layer.geometry),
308+
transforms: [
309+
Transform3(
310+
position: Position3(rect.x, rect.y, 0),
311+
scale: Size3(
312+
layerScale.width,
313+
layerScale.height,
314+
1
315+
)
316+
)
317+
],
318+
material: material,
319+
vsh: .standard,
320+
fsh: .textureSample,
321+
flags: .userInterface
322+
)
323+
)
324+
}else{
325+
// If the layer is unrenderable, draw the placeholder texture
326+
canvas.insert(
327+
DrawCommand(
328+
resource: .geometry(.rectOriginTopLeft),
329+
transforms: [
330+
Transform3(
331+
position: Position3(rect.x, rect.y, 0),
332+
scale: .one
333+
)
334+
],
335+
material: .init(texture: Texture(as: .checkerPattern), tintColor: .magenta),
336+
vsh: .standard,
337+
fsh: .textureSampleTintColor,
338+
flags: .userInterface
339+
)
340+
)
341+
}
342+
}
343+
}
344+
}
345+

0 commit comments

Comments
 (0)