2026-07-24 10:33:45 +02:00
|
|
|
|
internal import CPlaydate
|
|
|
|
|
|
|
2026-07-25 10:43:32 +02:00
|
|
|
|
private var spriteAPI: UnsafePointer<playdate_sprite> { Playdate.spriteAPI.unsafelyUnwrapped }
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// A drawable object with position, z-order, and collisions. Wraps `LCDSprite`; static
|
|
|
|
|
|
/// members wrap the global sprite functions. Retains its image, stencil, tilemap, closures.
|
|
|
|
|
|
/// The C userdata slot holds the wrapper back-reference; don't set it from C (use `userdata`).
|
|
|
|
|
|
/// `add()` retains the sprite until `remove()`/`removeAll()`. Owned sprites free their
|
|
|
|
|
|
/// `LCDSprite` on deinit; sprites created elsewhere get transient, non-owning wrappers.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public final class Sprite {
|
|
|
|
|
|
let pointer: OpaquePointer
|
|
|
|
|
|
let isOwned: Bool
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Index in `displayList`, or -1 when absent; makes `add()`/`remove()` O(1).
|
2026-07-25 10:43:32 +02:00
|
|
|
|
private var displayListIndex = -1
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Called by the C trampolines; resources retained so C never points at freed ones.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
var updateFunction: ((Sprite) -> Void)?
|
|
|
|
|
|
var drawFunction: ((Sprite, _ bounds: Rect, _ drawRect: Rect) -> Void)?
|
|
|
|
|
|
var collisionResponseFunction: ((Sprite, _ other: Sprite) -> CollisionResponse)?
|
|
|
|
|
|
private var retainedImage: Graphics.Bitmap?
|
|
|
|
|
|
private var retainedStencil: Graphics.Bitmap?
|
|
|
|
|
|
private var retainedTilemap: Graphics.TileMap?
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Free-form game storage. Not copied by `copy()`.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var userdata: AnyObject?
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
|
|
|
|
|
self.pointer = pointer
|
|
|
|
|
|
self.isOwned = isOwned
|
2026-09-18 13:15:08 +00:00
|
|
|
|
// Only owned wrappers clear the back-reference in deinit; others would dangle.
|
2026-07-25 10:28:24 +02:00
|
|
|
|
if isOwned {
|
|
|
|
|
|
spriteAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
|
|
|
|
|
}
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Allocates a sprite, not yet in the display list.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public convenience init() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
self.init(pointer: spriteAPI.pointee.newSprite.unsafelyUnwrapped().unsafelyUnwrapped, isOwned: true)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
deinit {
|
|
|
|
|
|
if isOwned {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, nil)
|
|
|
|
|
|
spriteAPI.pointee.freeSprite.unsafelyUnwrapped(pointer)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// The stored wrapper, or a transient non-owning one for sprites created elsewhere.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
static func wrapper(for pointer: OpaquePointer) -> Sprite {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
if let userdata = spriteAPI.pointee.getUserdata.unsafelyUnwrapped(pointer) {
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return Unmanaged<Sprite>.fromOpaque(userdata).takeUnretainedValue()
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return Sprite(pointer: pointer, isOwned: false)
|
|
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Also copies callbacks and the retained image, stencil, and tilemap; not `userdata`.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func copy() -> Sprite {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let copy = Sprite(pointer: spriteAPI.pointee.copy.unsafelyUnwrapped(pointer).unsafelyUnwrapped,
|
2026-07-24 11:10:25 +02:00
|
|
|
|
isOwned: true)
|
|
|
|
|
|
copy.updateFunction = updateFunction
|
|
|
|
|
|
copy.drawFunction = drawFunction
|
|
|
|
|
|
copy.collisionResponseFunction = collisionResponseFunction
|
|
|
|
|
|
copy.retainedImage = retainedImage
|
|
|
|
|
|
copy.retainedStencil = retainedStencil
|
|
|
|
|
|
copy.retainedTilemap = retainedTilemap
|
|
|
|
|
|
return copy
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
// MARK: - Display list and drawing
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Keeps added sprites alive while the C display list references them.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
nonisolated(unsafe) private static var displayList: [Sprite] = []
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// `true` redraws all sprites every frame; can be faster with many moving sprites.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func setAlwaysRedraw(_ flag: Bool) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setAlwaysRedraw.unsafelyUnwrapped(flag ? 1 : 0)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Marks `rect` (screen coordinates) dirty. Graphics drawing calls do this already.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func addDirtyRect(_ rect: Graphics.Rect) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.addDirtyRect.unsafelyUnwrapped(rect.cValue)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static func drawAll() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.drawSprites.unsafelyUnwrapped()
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Calls each sprite's update function, then draws all sprites.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func updateAndDrawAll() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.updateAndDrawSprites.unsafelyUnwrapped()
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Number of sprites in the display list.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static var count: Int {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
Int(spriteAPI.pointee.getSpriteCount.unsafelyUnwrapped())
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Adds to the display list; repeated adds retain only once.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func add() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.addSprite.unsafelyUnwrapped(pointer)
|
2026-07-25 10:43:32 +02:00
|
|
|
|
if displayListIndex < 0 {
|
|
|
|
|
|
displayListIndex = Sprite.displayList.count
|
2026-07-24 11:10:25 +02:00
|
|
|
|
Sprite.displayList.append(self)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func remove() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.removeSprite.unsafelyUnwrapped(pointer)
|
2026-07-25 10:43:32 +02:00
|
|
|
|
guard displayListIndex >= 0 else { return }
|
2026-09-18 13:15:08 +00:00
|
|
|
|
// Swap-remove: the keep-alive list is unordered (the OS keeps draw order).
|
2026-07-25 10:43:32 +02:00
|
|
|
|
let index = displayListIndex
|
|
|
|
|
|
let last = Sprite.displayList.removeLast()
|
|
|
|
|
|
if last !== self {
|
|
|
|
|
|
Sprite.displayList[index] = last
|
|
|
|
|
|
last.displayListIndex = index
|
|
|
|
|
|
}
|
|
|
|
|
|
displayListIndex = -1
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func remove(_ sprites: [Sprite]) {
|
|
|
|
|
|
for sprite in sprites { sprite.remove() }
|
|
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func removeAll() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.removeAllSprites.unsafelyUnwrapped()
|
2026-07-25 10:43:32 +02:00
|
|
|
|
for sprite in displayList { sprite.displayListIndex = -1 }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
displayList = []
|
|
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
// MARK: - Geometry
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var bounds: Rect {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { Rect(spriteAPI.pointee.getBounds.unsafelyUnwrapped(pointer)) }
|
|
|
|
|
|
set { spriteAPI.pointee.setBounds.unsafelyUnwrapped(pointer, newValue.cValue) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Moves so `center` is at (`x`, `y`), recomputing bounds from size and `center`.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func moveTo(x: Float, y: Float) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.moveTo.unsafelyUnwrapped(pointer, x, y)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func moveBy(dx: Float, dy: Float) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.moveBy.unsafelyUnwrapped(pointer, dx, dy)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Where the sprite's `center` point is.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var position: (x: Float, y: Float) {
|
|
|
|
|
|
var x: Float = 0, y: Float = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.getPosition.unsafelyUnwrapped(pointer, &x, &y)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return (x, y)
|
|
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Size `moveTo(x:y:)` uses to compute bounds.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setSize(width: Float, height: Float) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setSize.unsafelyUnwrapped(pointer, width, height)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Drawing center as a 0...1 fraction of size; (0, 0) is top left, (1, 1) bottom right.
|
|
|
|
|
|
/// Default (0.5, 0.5).
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var center: (x: Float, y: Float) {
|
|
|
|
|
|
get {
|
2026-07-24 10:33:45 +02:00
|
|
|
|
var x: Float = 0, y: Float = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.getCenter.unsafelyUnwrapped(pointer, &x, &y)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
return (x, y)
|
|
|
|
|
|
}
|
2026-07-25 07:23:19 +02:00
|
|
|
|
set { spriteAPI.pointee.setCenter.unsafelyUnwrapped(pointer, newValue.x, newValue.y) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Higher values draw on top.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var zIndex: Int16 {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { spriteAPI.pointee.getZIndex.unsafelyUnwrapped(pointer) }
|
|
|
|
|
|
set { spriteAPI.pointee.setZIndex.unsafelyUnwrapped(pointer, newValue) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
// MARK: - Appearance
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Sets the image, drawn with `flip`, and resizes bounds to match; `nil` removes it.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setImage(_ image: Graphics.Bitmap?, flip: Graphics.BitmapFlip = .unflipped) {
|
|
|
|
|
|
retainedImage = image
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setImage.unsafelyUnwrapped(pointer, image?.pointer, flip.cValue)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// The image from `setImage(_:flip:)`, else a non-owning wrapper of the C one, or `nil`.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var image: Graphics.Bitmap? {
|
|
|
|
|
|
if let retainedImage { return retainedImage }
|
2026-07-25 07:23:19 +02:00
|
|
|
|
guard let image = spriteAPI.pointee.getImage.unsafelyUnwrapped(pointer) else { return nil }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return Graphics.Bitmap(pointer: image, isOwned: false)
|
|
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// The tilemap set here. Setting resizes bounds to match; `nil` removes it.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var tilemap: Graphics.TileMap? {
|
|
|
|
|
|
get { retainedTilemap }
|
|
|
|
|
|
set {
|
|
|
|
|
|
retainedTilemap = newValue
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setTilemap.unsafelyUnwrapped(pointer, newValue?.pointer)
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-24 11:10:25 +02:00
|
|
|
|
|
|
|
|
|
|
public func setDrawMode(_ mode: Graphics.DrawMode) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setDrawMode.unsafelyUnwrapped(pointer, mode.cValue)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public var imageFlip: Graphics.BitmapFlip {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { Graphics.BitmapFlip(spriteAPI.pointee.getImageFlip.unsafelyUnwrapped(pointer)) }
|
|
|
|
|
|
set { spriteAPI.pointee.setImageFlip.unsafelyUnwrapped(pointer, newValue.cValue) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Pixels draw only where `stencil` is white. Screen space: it doesn't move with the
|
|
|
|
|
|
/// sprite. `nil` clears it. With `tile`, it repeats; width must be a multiple of 32.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setStencil(_ stencil: Graphics.Bitmap?, tile: Bool = false) {
|
|
|
|
|
|
retainedStencil = stencil
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setStencilImage.unsafelyUnwrapped(pointer, stencil?.pointer, tile ? 1 : 0)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Sets an 8×8 stencil pattern, one byte per row.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setStencilPattern(_ rows: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8)) {
|
2026-09-18 13:15:08 +00:00
|
|
|
|
// The tuple is 8 contiguous bytes and C copies them, so stack storage is safe.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
withUnsafeBytes(of: rows) { buffer in
|
|
|
|
|
|
let pattern = UnsafeMutablePointer(
|
|
|
|
|
|
mutating: buffer.baseAddress.unsafelyUnwrapped.assumingMemoryBound(to: UInt8.self))
|
|
|
|
|
|
spriteAPI.pointee.setStencilPattern.unsafelyUnwrapped(pointer, pattern)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// `InlineArray` overload of the tuple variant.
|
|
|
|
|
|
@available(macOS 26, *)
|
|
|
|
|
|
public func setStencilPattern(_ rows: [8 of UInt8]) {
|
|
|
|
|
|
// C copies the pattern, so passing the inline array's storage is safe.
|
|
|
|
|
|
rows.span.withUnsafeBufferPointer { buffer in
|
|
|
|
|
|
spriteAPI.pointee.setStencilPattern.unsafelyUnwrapped(
|
|
|
|
|
|
pointer, UnsafeMutablePointer(mutating: buffer.baseAddress))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func clearStencil() {
|
|
|
|
|
|
retainedStencil = nil
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.clearStencil.unsafelyUnwrapped(pointer)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// `rect` is in screen coordinates.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setClipRect(_ rect: Graphics.Rect) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setClipRect.unsafelyUnwrapped(pointer, rect.cValue)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public func clearClipRect() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.clearClipRect.unsafelyUnwrapped(pointer)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Clips sprites with a z-index in `startZ...endZ` (inclusive) to `rect`.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func setClipRectsInRange(_ rect: Graphics.Rect, startZ: Int, endZ: Int) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setClipRectsInRange.unsafelyUnwrapped(rect.cValue, Int32(startZ), Int32(endZ))
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Clears clip rects of sprites with a z-index in `startZ...endZ` (inclusive).
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func clearClipRectsInRange(startZ: Int, endZ: Int) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.clearClipRectsInRange.unsafelyUnwrapped(Int32(startZ), Int32(endZ))
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
// MARK: - Flags, redraw, and tag
|
2026-07-24 11:10:25 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Whether `updateAndDrawAll()` calls the update function.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var updatesEnabled: Bool {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { spriteAPI.pointee.updatesEnabled.unsafelyUnwrapped(pointer) != 0 }
|
|
|
|
|
|
set { spriteAPI.pointee.setUpdatesEnabled.unsafelyUnwrapped(pointer, newValue ? 1 : 0) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Also requires a `collideRect`. Default `true`.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var collisionsEnabled: Bool {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { spriteAPI.pointee.collisionsEnabled.unsafelyUnwrapped(pointer) != 0 }
|
|
|
|
|
|
set { spriteAPI.pointee.setCollisionsEnabled.unsafelyUnwrapped(pointer, newValue ? 1 : 0) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public var isVisible: Bool {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { spriteAPI.pointee.isVisible.unsafelyUnwrapped(pointer) != 0 }
|
|
|
|
|
|
set { spriteAPI.pointee.setVisible.unsafelyUnwrapped(pointer, newValue ? 1 : 0) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Opaque sprites hide what's behind them. Set automatically for images without a mask.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setOpaque(_ flag: Bool) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setOpaque.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public func markDirty() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.markDirty.unsafelyUnwrapped(pointer)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// `rect` is relative to the sprite's top-left corner.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func markDirty(rect: Rect) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.markDirtyRect.unsafelyUnwrapped(pointer, rect.cValue)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Game-defined tag, 0–255, e.g. for collision handling.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var tag: UInt8 {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { spriteAPI.pointee.getTag.unsafelyUnwrapped(pointer) }
|
|
|
|
|
|
set { spriteAPI.pointee.setTag.unsafelyUnwrapped(pointer, newValue) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// `true` draws in screen coordinates; collisions stay in world space.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setIgnoresDrawOffset(_ flag: Bool) {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setIgnoresDrawOffset.unsafelyUnwrapped(pointer, flag ? 1 : 0)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Callbacks
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Called by `updateAndDrawAll()`; `nil` removes it.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setUpdateFunction(_ update: ((Sprite) -> Void)?) {
|
|
|
|
|
|
updateFunction = update
|
|
|
|
|
|
if update != nil {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setUpdateFunction.unsafelyUnwrapped(pointer, { spritePointer in
|
2026-07-24 11:10:25 +02:00
|
|
|
|
guard let spritePointer else { return }
|
|
|
|
|
|
let sprite = Sprite.wrapper(for: spritePointer)
|
|
|
|
|
|
sprite.updateFunction?(sprite)
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setUpdateFunction.unsafelyUnwrapped(pointer, nil)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Receives `bounds` and the dirty `drawRect`; `nil` removes it. Runs only while on
|
|
|
|
|
|
/// screen with a size (from `setSize(width:height:)` or `bounds`).
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setDrawFunction(_ draw: ((Sprite, _ bounds: Rect, _ drawRect: Rect) -> Void)?) {
|
|
|
|
|
|
drawFunction = draw
|
|
|
|
|
|
if draw != nil {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setDrawFunction.unsafelyUnwrapped(pointer, { spritePointer, bounds, drawRect in
|
2026-07-24 11:10:25 +02:00
|
|
|
|
guard let spritePointer else { return }
|
|
|
|
|
|
let sprite = Sprite.wrapper(for: spritePointer)
|
|
|
|
|
|
sprite.drawFunction?(sprite, Rect(bounds), Rect(drawRect))
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setDrawFunction.unsafelyUnwrapped(pointer, nil)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Collisions
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Frees and reallocates the collision data, resetting it. Call when changing scenes.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func resetCollisionWorld() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.resetCollisionWorld.unsafelyUnwrapped()
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Relative to the sprite's bounds.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var collideRect: Rect {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
get { Rect(spriteAPI.pointee.getCollideRect.unsafelyUnwrapped(pointer)) }
|
|
|
|
|
|
set { spriteAPI.pointee.setCollideRect.unsafelyUnwrapped(pointer, newValue.cValue) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public func clearCollideRect() {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.clearCollideRect.unsafelyUnwrapped(pointer)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Chooses this sprite's response when colliding with `other`; `nil` removes it.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func setCollisionResponseFunction(_ filter: ((Sprite, _ other: Sprite) -> CollisionResponse)?) {
|
|
|
|
|
|
collisionResponseFunction = filter
|
|
|
|
|
|
if filter != nil {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setCollisionResponseFunction.unsafelyUnwrapped(pointer, { spritePointer, otherPointer in
|
2026-07-24 11:10:25 +02:00
|
|
|
|
guard let spritePointer, let otherPointer else { return kCollisionTypeFreeze }
|
|
|
|
|
|
let sprite = Sprite.wrapper(for: spritePointer)
|
|
|
|
|
|
let other = Sprite.wrapper(for: otherPointer)
|
|
|
|
|
|
return sprite.collisionResponseFunction?(sprite, other).cValue ?? kCollisionTypeFreeze
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
2026-07-25 07:23:19 +02:00
|
|
|
|
spriteAPI.pointee.setCollisionResponseFunction.unsafelyUnwrapped(pointer, nil)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Visits each entry of a C collision array, then frees it (C transfers ownership).
|
2026-07-25 10:43:32 +02:00
|
|
|
|
private static func visitCollisions(_ pointer: UnsafeMutablePointer<SpriteCollisionInfo>?,
|
|
|
|
|
|
count: Int32, _ visit: (CollisionInfo) -> Void) {
|
|
|
|
|
|
guard let pointer else { return }
|
|
|
|
|
|
for index in 0..<Int(count) {
|
|
|
|
|
|
visit(CollisionInfo(pointer[index]))
|
|
|
|
|
|
}
|
|
|
|
|
|
System.systemFree(pointer)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:10:25 +02:00
|
|
|
|
/// Converts and frees a C collision info array.
|
|
|
|
|
|
private static func collisionInfos(_ pointer: UnsafeMutablePointer<SpriteCollisionInfo>?,
|
|
|
|
|
|
count: Int32) -> [CollisionInfo] {
|
|
|
|
|
|
var infos = [CollisionInfo]()
|
|
|
|
|
|
infos.reserveCapacity(Int(count))
|
2026-07-25 10:43:32 +02:00
|
|
|
|
visitCollisions(pointer, count: count) { infos.append($0) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return infos
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Where a move toward the goal would end and what it would hit, without moving.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public func checkCollisions(goalX: Float, goalY: Float)
|
|
|
|
|
|
-> (actual: (x: Float, y: Float), collisions: [CollisionInfo]) {
|
|
|
|
|
|
var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let result = spriteAPI.pointee.checkCollisions.unsafelyUnwrapped(
|
2026-07-24 11:10:25 +02:00
|
|
|
|
pointer, goalX, goalY, &actualX, &actualY, &count)
|
|
|
|
|
|
return ((actualX, actualY), Sprite.collisionInfos(result, count: count))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `checkCollisions(goalX:goalY:)`, but visits each collision without allocating.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public func checkCollisions(goalX: Float, goalY: Float,
|
|
|
|
|
|
_ visit: (CollisionInfo) -> Void) -> (x: Float, y: Float) {
|
|
|
|
|
|
var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0
|
|
|
|
|
|
let result = spriteAPI.pointee.checkCollisions.unsafelyUnwrapped(
|
|
|
|
|
|
pointer, goalX, goalY, &actualX, &actualY, &count)
|
|
|
|
|
|
Sprite.visitCollisions(result, count: count, visit)
|
|
|
|
|
|
return (actualX, actualY)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Moves toward the goal, resolving collisions. Returns the final position (the goal if
|
|
|
|
|
|
/// nothing was hit) and the collisions.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
|
public func moveWithCollisions(goalX: Float, goalY: Float)
|
|
|
|
|
|
-> (actual: (x: Float, y: Float), collisions: [CollisionInfo]) {
|
|
|
|
|
|
var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let result = spriteAPI.pointee.moveWithCollisions.unsafelyUnwrapped(
|
2026-07-24 11:10:25 +02:00
|
|
|
|
pointer, goalX, goalY, &actualX, &actualY, &count)
|
|
|
|
|
|
return ((actualX, actualY), Sprite.collisionInfos(result, count: count))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `moveWithCollisions(goalX:goalY:)`, but visits collisions without allocating.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
@discardableResult
|
|
|
|
|
|
public func moveWithCollisions(goalX: Float, goalY: Float,
|
|
|
|
|
|
_ visit: (CollisionInfo) -> Void) -> (x: Float, y: Float) {
|
|
|
|
|
|
var actualX: Float = 0, actualY: Float = 0, count: Int32 = 0
|
|
|
|
|
|
let result = spriteAPI.pointee.moveWithCollisions.unsafelyUnwrapped(
|
|
|
|
|
|
pointer, goalX, goalY, &actualX, &actualY, &count)
|
|
|
|
|
|
Sprite.visitCollisions(result, count: count, visit)
|
|
|
|
|
|
return (actualX, actualY)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Visits non-null entries of a C sprite array, then frees it (C transfers ownership).
|
2026-07-25 10:43:32 +02:00
|
|
|
|
private static func visitSprites(_ pointer: UnsafeMutablePointer<OpaquePointer?>?,
|
|
|
|
|
|
count: Int32, _ visit: (Sprite) -> Void) {
|
|
|
|
|
|
guard let pointer else { return }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
for index in 0..<Int(count) {
|
|
|
|
|
|
if let spritePointer = pointer[index] {
|
2026-07-25 10:43:32 +02:00
|
|
|
|
visit(wrapper(for: spritePointer))
|
2026-07-24 11:10:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
System.systemFree(pointer)
|
2026-07-25 10:43:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Converts and frees a C sprite pointer array.
|
|
|
|
|
|
private static func sprites(_ pointer: UnsafeMutablePointer<OpaquePointer?>?,
|
|
|
|
|
|
count: Int32) -> [Sprite] {
|
|
|
|
|
|
var sprites = [Sprite]()
|
|
|
|
|
|
sprites.reserveCapacity(Int(count))
|
|
|
|
|
|
visitSprites(pointer, count: count) { sprites.append($0) }
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return sprites
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Sprites whose collide rects contain (`x`, `y`).
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func query(atPoint x: Float, _ y: Float) -> [Sprite] {
|
|
|
|
|
|
var count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let result = spriteAPI.pointee.querySpritesAtPoint.unsafelyUnwrapped(x, y, &count)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return sprites(result, count: count)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `query(atPoint:_:)`, but visits each sprite without allocating.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public static func query(atPoint x: Float, _ y: Float, _ visit: (Sprite) -> Void) {
|
|
|
|
|
|
var count: Int32 = 0
|
|
|
|
|
|
let result = spriteAPI.pointee.querySpritesAtPoint.unsafelyUnwrapped(x, y, &count)
|
|
|
|
|
|
visitSprites(result, count: count, visit)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Sprites whose collide rects intersect the `width` × `height` rect at (`x`, `y`).
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func query(inRect x: Float, _ y: Float, width: Float, height: Float) -> [Sprite] {
|
|
|
|
|
|
var count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let result = spriteAPI.pointee.querySpritesInRect.unsafelyUnwrapped(x, y, width, height, &count)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return sprites(result, count: count)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `query(inRect:_:width:height:)`, but visits each sprite without allocating.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public static func query(inRect x: Float, _ y: Float, width: Float, height: Float,
|
|
|
|
|
|
_ visit: (Sprite) -> Void) {
|
|
|
|
|
|
var count: Int32 = 0
|
|
|
|
|
|
let result = spriteAPI.pointee.querySpritesInRect.unsafelyUnwrapped(x, y, width, height, &count)
|
|
|
|
|
|
visitSprites(result, count: count, visit)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Sprites whose collide rects intersect the segment (`x1`, `y1`)–(`x2`, `y2`).
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func query(alongLine x1: Float, _ y1: Float, _ x2: Float, _ y2: Float) -> [Sprite] {
|
|
|
|
|
|
var count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let result = spriteAPI.pointee.querySpritesAlongLine.unsafelyUnwrapped(x1, y1, x2, y2, &count)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return sprites(result, count: count)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `query(alongLine:_:_:_:)`, but visits each sprite without allocating.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public static func query(alongLine x1: Float, _ y1: Float, _ x2: Float, _ y2: Float,
|
|
|
|
|
|
_ visit: (Sprite) -> Void) {
|
|
|
|
|
|
var count: Int32 = 0
|
|
|
|
|
|
let result = spriteAPI.pointee.querySpritesAlongLine.unsafelyUnwrapped(x1, y1, x2, y2, &count)
|
|
|
|
|
|
visitSprites(result, count: count, visit)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `query(alongLine:_:_:_:)`, plus entry/exit details. Slower; use only if needed.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static func queryInfo(alongLine x1: Float, _ y1: Float,
|
|
|
|
|
|
_ x2: Float, _ y2: Float) -> [QueryInfo] {
|
|
|
|
|
|
var count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
guard let result = spriteAPI.pointee.querySpriteInfoAlongLine.unsafelyUnwrapped(
|
2026-07-24 11:10:25 +02:00
|
|
|
|
x1, y1, x2, y2, &count) else { return [] }
|
|
|
|
|
|
var infos = [QueryInfo]()
|
|
|
|
|
|
infos.reserveCapacity(Int(count))
|
|
|
|
|
|
for index in 0..<Int(count) {
|
|
|
|
|
|
infos.append(QueryInfo(result[index]))
|
|
|
|
|
|
}
|
|
|
|
|
|
System.systemFree(result)
|
|
|
|
|
|
return infos
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Sprites whose collide rects overlap this sprite's.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public var overlappingSprites: [Sprite] {
|
|
|
|
|
|
var count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let result = spriteAPI.pointee.overlappingSprites.unsafelyUnwrapped(pointer, &count)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return Sprite.sprites(result, count: count)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `overlappingSprites`, but visits each sprite without allocating.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public func overlappingSprites(_ visit: (Sprite) -> Void) {
|
|
|
|
|
|
var count: Int32 = 0
|
|
|
|
|
|
let result = spriteAPI.pointee.overlappingSprites.unsafelyUnwrapped(pointer, &count)
|
|
|
|
|
|
Sprite.visitSprites(result, count: count, visit)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// All overlapping sprites as consecutive pairs: [0] and [1] overlap, [2] and [3], etc.
|
2026-07-24 11:10:25 +02:00
|
|
|
|
public static var allOverlappingSprites: [Sprite] {
|
|
|
|
|
|
var count: Int32 = 0
|
2026-07-25 07:23:19 +02:00
|
|
|
|
let result = spriteAPI.pointee.allOverlappingSprites.unsafelyUnwrapped(&count)
|
2026-07-24 11:10:25 +02:00
|
|
|
|
return sprites(result, count: count)
|
|
|
|
|
|
}
|
2026-07-25 10:43:32 +02:00
|
|
|
|
|
2026-09-18 13:15:08 +00:00
|
|
|
|
/// Like `allOverlappingSprites`, but visits sprites (same pair order) without allocating.
|
2026-07-25 10:43:32 +02:00
|
|
|
|
public static func allOverlappingSprites(_ visit: (Sprite) -> Void) {
|
|
|
|
|
|
var count: Int32 = 0
|
|
|
|
|
|
let result = spriteAPI.pointee.allOverlappingSprites.unsafelyUnwrapped(&count)
|
|
|
|
|
|
visitSprites(result, count: count, visit)
|
|
|
|
|
|
}
|
2026-07-24 10:33:45 +02:00
|
|
|
|
}
|