├── showcase.png
├── MiniBlocks.swiftpm
├── Sources
│ ├── Utils
│ │ ├── ChunkConstants.swift
│ │ ├── SceneKitUtils.swift
│ │ ├── Vec2.swift
│ │ ├── Box.swift
│ │ ├── Vec3.swift
│ │ ├── BlockPos2.swift
│ │ ├── Vec2Convertible.swift
│ │ ├── BlockPos3.swift
│ │ ├── Vec3Convertible.swift
│ │ ├── MathUtils.swift
│ │ ├── GameControllerUtils.swift
│ │ ├── FIFOCache.swift
│ │ ├── Wraparound.swift
│ │ ├── GridIterator2.swift
│ │ ├── ChunkRegion.swift
│ │ ├── ChunkPos.swift
│ │ ├── Throttler.swift
│ │ ├── CompatibilityLayer.swift
│ │ ├── CoreGraphicsUtils.swift
│ │ ├── Deque.swift
│ │ ├── Debouncer.swift
│ │ ├── Vec2Protocol.swift
│ │ ├── CyclicDeque.swift
│ │ └── Vec3Protocol.swift
│ ├── Model
│ │ ├── Item.swift
│ │ ├── ItemStack.swift
│ │ ├── ItemType.swift
│ │ ├── Block.swift
│ │ ├── InventoryConstants.swift
│ │ ├── EmptyWorldGenerator.swift
│ │ ├── FlatWorldGenerator.swift
│ │ ├── WorldGenerator.swift
│ │ ├── GameMode.swift
│ │ ├── WavyHillsWorldGenerator.swift
│ │ ├── OceanWorldGenerator.swift
│ │ ├── BlockType.swift
│ │ ├── Inventory.swift
│ │ ├── Strip.swift
│ │ ├── WorldGeneratorType.swift
│ │ ├── PlayerInfo.swift
│ │ ├── NatureWorldGenerator.swift
│ │ ├── Achievements.swift
│ │ └── World.swift
│ ├── Component
│ │ ├── HandNodeComponent.swift
│ │ ├── NameComponent.swift
│ │ ├── SceneNodeComponent.swift
│ │ ├── SpriteNodeComponent.swift
│ │ ├── WorldComponent.swift
│ │ ├── HeightAboveGroundComponent.swift
│ │ ├── TouchInteractable.swift
│ │ ├── WorldAssociationComponent.swift
│ │ ├── MouseCaptureVisibilityComponent.swift
│ │ ├── HotbarHUDControlComponent.swift
│ │ ├── AchievementHUDEntity.swift
│ │ ├── PlayerAssocationComponent.swift
│ │ ├── PlayerPositioningComponent.swift
│ │ ├── LookAtBlockComponent.swift
│ │ ├── HandLoadComponent.swift
│ │ ├── PlayerGravityComponent.swift
│ │ ├── DebugHUDLoadComponent.swift
│ │ ├── ControlPadHUDControlComponent.swift
│ │ ├── AchievementHUDLoadComponent.swift
│ │ ├── WorldRetainComponent.swift
│ │ ├── HotbarHUDLoadComponent.swift
│ │ ├── WorldLoadComponent.swift
│ │ └── PlayerControlComponent.swift
│ ├── Node
│ │ ├── NodeConstants.swift
│ │ ├── CrosshairHUDNode.swift
│ │ ├── ControlPadHUDNode.swift
│ │ ├── HotbarHUDSlotNode.swift
│ │ ├── ItemNode.swift
│ │ ├── PauseHUDNode.swift
│ │ ├── AchievementHUDNode.swift
│ │ └── BlockNode.swift
│ ├── Entity
│ │ ├── AmbientLightEntity.swift
│ │ ├── SunEntity.swift
│ │ ├── PauseHUDEntity.swift
│ │ ├── CrosshairHUDEntity.swift
│ │ ├── HotbarHUDEntity.swift
│ │ ├── ControlPadHUDEntity.swift
│ │ ├── WorldEntity.swift
│ │ ├── DebugHUDEntity.swift
│ │ └── PlayerEntity.swift
│ └── View
│ │ ├── MiniBlocksSceneView.swift
│ │ ├── KeyCode.swift
│ │ └── MiniBlocksViewController.swift
├── App
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── 120.png
│ │ │ ├── 152.png
│ │ │ ├── 167.png
│ │ │ ├── 180.png
│ │ │ ├── 20.png
│ │ │ ├── 29.png
│ │ │ ├── 40.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ ├── 1024.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── MiniBlocksApp.swift
│ └── MiniBlocksView.swift
├── Resources
│ ├── TextureGrass.png
│ ├── TextureSand.png
│ ├── TextureStone.png
│ ├── TextureWater.png
│ ├── TextureWood.png
│ ├── MiniBlocksScene.scn
│ ├── TextureBedrock.png
│ └── TextureLeaves.png
└── Package.swift
├── MiniBlocks
├── MiniBlocks
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── 16.png
│ │ │ ├── 32.png
│ │ │ ├── 64.png
│ │ │ ├── 1024.png
│ │ │ ├── 128.png
│ │ │ ├── 256.png
│ │ │ ├── 512.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── main.swift
│ ├── MiniBlocks.entitlements
│ └── AppDelegate.swift
├── MiniBlocksToGo
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── 120.png
│ │ │ ├── 152.png
│ │ │ ├── 167.png
│ │ │ ├── 180.png
│ │ │ ├── 20.png
│ │ │ ├── 29.png
│ │ │ ├── 40.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ ├── 1024.png
│ │ │ └── Contents.json
│ │ └── AccentColor.colorset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ └── AppDelegate.swift
├── MiniBlocks.xcodeproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── MiniBlocks.xcscheme
└── MiniBlocksTests
│ ├── Utils
│ ├── GridIterator2Tests.swift
│ ├── ThrottlerTests.swift
│ ├── ChunkPosTests.swift
│ ├── MathUtilsTests.swift
│ ├── DebouncerTests.swift
│ └── CyclicDequeTests.swift
│ └── Model
│ └── AchievementsTests.swift
├── .gitignore
├── .github
└── workflows
│ └── build.yml
└── README.md
/showcase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/showcase.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/ChunkConstants.swift:
--------------------------------------------------------------------------------
1 | enum ChunkConstants {
2 | static let size: Int = 8
3 | }
4 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/TextureGrass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/TextureGrass.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/TextureSand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/TextureSand.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/TextureStone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/TextureStone.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/TextureWater.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/TextureWater.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/TextureWood.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/TextureWood.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/Item.swift:
--------------------------------------------------------------------------------
1 | /// An inventory item.
2 | struct Item: Hashable, Codable {
3 | let type: ItemType
4 | }
5 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/MiniBlocksScene.scn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/MiniBlocksScene.scn
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/TextureBedrock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/TextureBedrock.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Resources/TextureLeaves.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/Resources/TextureLeaves.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/ItemStack.swift:
--------------------------------------------------------------------------------
1 | struct ItemStack: Codable, Hashable {
2 | var item: Item
3 | var count: Int = 1
4 | }
5 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/ItemType.swift:
--------------------------------------------------------------------------------
1 | /// A 'kind' of item.
2 | enum ItemType: Codable, Hashable {
3 | case block(BlockType)
4 | }
5 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/Block.swift:
--------------------------------------------------------------------------------
1 | /// A block in the world without a position.
2 | struct Block: Hashable, Codable {
3 | let type: BlockType
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .build
3 | .swiftpm
4 | .vscode
5 | __pycache__
6 | *.pyc
7 | *~
8 | Packages
9 | xcuserdata/
10 | build/
11 | node_modules
12 | local
13 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/InventoryConstants.swift:
--------------------------------------------------------------------------------
1 | enum InventoryConstants {
2 | static let inventorySlotCount = 24
3 | static let hotbarSlotCount = 8
4 | }
5 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/64.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/SceneKitUtils.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 |
3 | // Adds the missing operator overloads to SceneKit vectors.
4 | extension SCNVector3: Vec3Protocol {}
5 |
6 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fwcd/mini-blocks/HEAD/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/HandNodeComponent.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 | import GameplayKit
3 |
4 | /// Indicates that the node includes a node that represents a held item.
5 | class HandNodeComponent: SceneNodeComponent {}
6 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/EmptyWorldGenerator.swift:
--------------------------------------------------------------------------------
1 | /// Generates a void world with no blocks.
2 | struct EmptyWorldGenerator: WorldGenerator {
3 | func generate(at pos: BlockPos2) -> Strip {
4 | Strip()
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/FlatWorldGenerator.swift:
--------------------------------------------------------------------------------
1 | /// Generates a flat grass world.
2 | struct FlatWorldGenerator: WorldGenerator {
3 | func generate(at pos: BlockPos2) -> Strip {
4 | Strip(blocks: [0: Block(type: .grass)])
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/NodeConstants.swift:
--------------------------------------------------------------------------------
1 | enum NodeConstants {
2 | static let fontName = "American Typewriter"
3 | static let overlayBackground: Color = .black.withAlphaComponent(0.9)
4 | static let foregroundColor: Color = .white
5 | }
6 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Vec2.swift:
--------------------------------------------------------------------------------
1 | struct Vec2: Hashable, Codable, Vec2Protocol {
2 | var x: Double
3 | var z: Double
4 |
5 | init(x: Double = 0, z: Double = 0) {
6 | self.x = x
7 | self.z = z
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/MiniBlocksApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct MiniBlocksApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | MiniBlocksView()
8 | .ignoresSafeArea()
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/WorldGenerator.swift:
--------------------------------------------------------------------------------
1 | /// A procedural world generator, i.e. a function that generates strips of the world.
2 | protocol WorldGenerator {
3 | /// Generates a strip/slice of the world.
4 | func generate(at pos: BlockPos2) -> Strip
5 | }
6 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Box.swift:
--------------------------------------------------------------------------------
1 | /// A wrapper that adds reference semantics/shared ownership to a value.
2 | @propertyWrapper
3 | class Box {
4 | var wrappedValue: Wrapped
5 |
6 | init(wrappedValue: Wrapped) {
7 | self.wrappedValue = wrappedValue
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Vec3.swift:
--------------------------------------------------------------------------------
1 | struct Vec3: Hashable, Codable, Vec3Protocol {
2 | var x: Double
3 | var y: Double
4 | var z: Double
5 |
6 | init(x: Double = 0, y: Double = 0, z: Double = 0) {
7 | self.x = x
8 | self.y = y
9 | self.z = z
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/main.swift:
--------------------------------------------------------------------------------
1 | //
2 | // main.swift
3 | // MiniBlocks
4 | //
5 | // Created by Fredrik on 06.01.22.
6 | //
7 |
8 | import Cocoa
9 |
10 | let app = NSApplication.shared
11 | let delegate = AppDelegate()
12 | app.delegate = delegate
13 |
14 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
15 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/GameMode.swift:
--------------------------------------------------------------------------------
1 | public enum GameMode: Int, Codable, Hashable {
2 | case creative = 0
3 | case survival
4 |
5 | var permitsFlight: Bool {
6 | [.creative].contains(self)
7 | }
8 |
9 | var enablesGravityAndCollisions: Bool {
10 | [.survival].contains(self)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/NameComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | /// Lets the associated entity have a name.
4 | class NameComponent: GKComponent {
5 | let name: String
6 |
7 | init(name: String) {
8 | self.name = name
9 | super.init()
10 | }
11 |
12 | required init?(coder: NSCoder) {
13 | nil
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/BlockPos2.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 |
3 | /// A flat position on the block grid.
4 | struct BlockPos2: Hashable, Codable, Vec2Protocol, Vec3Convertible {
5 | typealias AssociatedVec3 = BlockPos3
6 |
7 | var x: Int
8 | var z: Int
9 |
10 | init(x: Int = 0, z: Int = 0) {
11 | self.x = x
12 | self.z = z
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Vec2Convertible.swift:
--------------------------------------------------------------------------------
1 | protocol Vec2Convertible {
2 | associatedtype AssociatedVec2: Vec2Protocol
3 |
4 | var asVec2: AssociatedVec2 { get }
5 | }
6 |
7 | extension Vec2Convertible where Self: Vec3Protocol, Coordinate == AssociatedVec2.Coordinate {
8 | var asVec2: AssociatedVec2 {
9 | AssociatedVec2(x: x, z: z)
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/MiniBlocks.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/WavyHillsWorldGenerator.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Generates a world from a periodic wave pattern.
4 | struct WavyHillsWorldGenerator: WorldGenerator {
5 | func generate(at pos: BlockPos2) -> Strip {
6 | let y = Int((-5 * sin(Float(pos.x) / 10) * cos(Float(pos.z) / 10)).rounded())
7 | return Strip(blocks: [y: Block(type: .grass)])
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/SceneNodeComponent.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 | import GameplayKit
3 |
4 | /// Adds a SceneKit node to the corresponding entity.
5 | class SceneNodeComponent: GKComponent {
6 | let node: SCNNode
7 |
8 | init(node: SCNNode) {
9 | self.node = node
10 | super.init()
11 | }
12 |
13 | required init?(coder: NSCoder) {
14 | nil
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/SpriteNodeComponent.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | /// Adds a SpriteKit node to the corresponding entity.
5 | class SpriteNodeComponent: GKComponent {
6 | let node: SKNode
7 |
8 | init(node: SKNode) {
9 | self.node = node
10 | super.init()
11 | }
12 |
13 | required init?(coder: NSCoder) {
14 | nil
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/WorldComponent.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 | import GameplayKit
3 |
4 | /// Lets the associated entity store the corresponding world.
5 | class WorldComponent: GKComponent {
6 | var world: World
7 |
8 | init(world: World) {
9 | self.world = world
10 | super.init()
11 | }
12 |
13 | required init?(coder: NSCoder) {
14 | nil
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/BlockPos3.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 |
3 | /// A position in the 3D block grid.
4 | struct BlockPos3: Hashable, Codable, Vec3Protocol, Vec2Convertible {
5 | typealias AssociatedVec2 = BlockPos2
6 |
7 | var x: Int
8 | var y: Int
9 | var z: Int
10 |
11 | init(x: Int = 0, y: Int = 0, z: Int = 0) {
12 | self.x = x
13 | self.y = y
14 | self.z = z
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Vec3Convertible.swift:
--------------------------------------------------------------------------------
1 | protocol Vec3Convertible {
2 | associatedtype AssociatedVec3: Vec3Protocol
3 |
4 | func with(y: AssociatedVec3.Coordinate) -> AssociatedVec3
5 | }
6 |
7 | extension Vec3Convertible where Self: Vec2Protocol, Coordinate == AssociatedVec3.Coordinate {
8 | func with(y: AssociatedVec3.Coordinate) -> AssociatedVec3 {
9 | AssociatedVec3(x: x, y: y, z: z)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/MathUtils.swift:
--------------------------------------------------------------------------------
1 | extension SignedInteger {
2 | /// Performs floor division with another integer.
3 | func floorDiv(_ rhs: Self) -> Self {
4 | assert(rhs > 0)
5 | return self >= 0 ? (self / rhs) : ((self - rhs + 1) / rhs)
6 | }
7 |
8 | /// Performs floor (clocklike) modulo with another integer.
9 | func floorMod(_ rhs: Self) -> Self {
10 | self - floorDiv(rhs) * rhs
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/OceanWorldGenerator.swift:
--------------------------------------------------------------------------------
1 | /// Generates a world that predominantly consists of water.
2 | struct OceanWorldGenerator: WorldGenerator {
3 | var depth: Int = 8
4 |
5 | func generate(at pos: BlockPos2) -> Strip {
6 | let blocks = Array(repeating: Block(type: .water), count: depth) + [Block(type: .stone)]
7 | return Strip(blocks: Dictionary(uniqueKeysWithValues: blocks.enumerated().map { (i, b) in (-i, b) }))
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/AmbientLightEntity.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 | import SceneKit
3 |
4 | func makeAmbientLightEntity() -> GKEntity {
5 | // Create node
6 | let light = SCNLight()
7 | light.type = .ambient
8 | light.color = Color.gray
9 | let node = SCNNode()
10 | node.light = light
11 |
12 | // Create entity
13 | let entity = GKEntity()
14 | entity.addComponent(SceneNodeComponent(node: node))
15 |
16 | return entity
17 | }
18 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.090",
9 | "green" : "0.212",
10 | "red" : "0.271"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/MiniBlocksView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UIKit
3 |
4 | struct MiniBlocksView: UIViewControllerRepresentable {
5 | func updateUIViewController(_ vc: MiniBlocksViewController, context: Context) {
6 | // Do nothing
7 | }
8 |
9 | func makeUIViewController(context: Context) -> MiniBlocksViewController {
10 | MiniBlocksViewController(
11 | sceneFrame: UIScreen.main.bounds,
12 | ambientOcclusionEnabled: true
13 | )
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/BlockType.swift:
--------------------------------------------------------------------------------
1 | /// A 'kind' of block.
2 | enum BlockType: Int, Hashable, Codable {
3 | case grass = 0
4 | case sand
5 | case stone
6 | case water
7 | case wood
8 | case leaves
9 | case bedrock
10 |
11 | var isTranslucent: Bool {
12 | [.water, .leaves].contains(self)
13 | }
14 |
15 | var isOpaque: Bool {
16 | !isTranslucent
17 | }
18 |
19 | var isLiquid: Bool {
20 | self == .water
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/SunEntity.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 | import SceneKit
3 |
4 | func makeSunEntity() -> GKEntity {
5 | // Create node
6 | let light = SCNLight()
7 | light.type = .directional
8 | light.color = Color.white
9 | let node = SCNNode()
10 | node.light = light
11 | node.eulerAngles = SCNVector3(x: -.pi / 3, y: -.pi / 3, z: 0)
12 |
13 | // Create entity
14 | let entity = GKEntity()
15 | entity.addComponent(SceneNodeComponent(node: node))
16 |
17 | return entity
18 | }
19 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/GameControllerUtils.swift:
--------------------------------------------------------------------------------
1 | import GameController
2 |
3 | extension GCKeyCode {
4 | var numericValue: Int? {
5 | switch self {
6 | case .zero: return 0
7 | case .one: return 1
8 | case .two: return 2
9 | case .three: return 3
10 | case .four: return 4
11 | case .five: return 5
12 | case .six: return 6
13 | case .seven: return 7
14 | case .eight: return 8
15 | case .nine: return 9
16 | default: return nil
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/PauseHUDEntity.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | func makePauseHUDEntity(in frame: CGRect, fontSize: CGFloat = 28) -> GKEntity {
5 | // Create node
6 | let node = makePauseHUDNode(size: frame.size, fontSize: fontSize)
7 | node.position = CGPoint(x: frame.midX, y: frame.midY)
8 |
9 | // Create entity
10 | let entity = GKEntity()
11 | entity.addComponent(SpriteNodeComponent(node: node))
12 | entity.addComponent(MouseCaptureVisibilityComponent(visibleWhenCaptured: false))
13 |
14 | return entity
15 | }
16 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/HeightAboveGroundComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | /// Lets the node 'float' the given distance above the ground. Applies e.g. to gravity calculations.
4 | class HeightAboveGroundComponent: GKComponent {
5 | let heightAboveGround: Double
6 |
7 | var offset: Vec3 {
8 | Vec3(x: 0, y: -heightAboveGround, z: 0)
9 | }
10 |
11 | init(heightAboveGround: Double) {
12 | self.heightAboveGround = heightAboveGround
13 | super.init()
14 | }
15 |
16 | required init?(coder: NSCoder) {
17 | nil
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/CrosshairHUDNode.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | private func makePartNode(size: CGSize) -> SKNode {
4 | let node = SKShapeNode(rect: CGRect(center: CGPoint(x: 0, y: 0), size: size))
5 | node.fillColor = .white
6 | node.strokeColor = .clear
7 | return node
8 | }
9 |
10 | func makeCrosshairHUDNode(size: CGFloat, thickness: CGFloat) -> SKNode {
11 | let node = SKNode()
12 | node.addChild(makePartNode(size: CGSize(width: size, height: thickness)))
13 | node.addChild(makePartNode(size: CGSize(width: thickness, height: size)))
14 | return node
15 | }
16 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/Inventory.swift:
--------------------------------------------------------------------------------
1 | /// An ordered container for items.
2 | struct Inventory: Codable, Hashable, Sequence {
3 | private(set) var slots: [Int: ItemStack] = [:]
4 | let slotCount: Int
5 |
6 | // TODO: Enforce slot count?
7 |
8 | init(slotCount: Int) {
9 | self.slotCount = slotCount
10 | }
11 |
12 | subscript(i: Int) -> ItemStack? {
13 | get { slots[i] }
14 | set { slots[i] = newValue }
15 | }
16 |
17 | func makeIterator() -> Dictionary.Iterator {
18 | slots.makeIterator()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/CrosshairHUDEntity.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | func makeCrosshairHUDEntity(size: CGFloat = 20, thickness: CGFloat = 2, in frame: CGRect) -> GKEntity {
5 | // Create node
6 | let node = makeCrosshairHUDNode(size: size, thickness: thickness)
7 | node.position = CGPoint(x: frame.midX, y: frame.midY)
8 |
9 | // Create entity
10 | let entity = GKEntity()
11 | entity.addComponent(SpriteNodeComponent(node: node))
12 | entity.addComponent(MouseCaptureVisibilityComponent(visibleWhenCaptured: true))
13 |
14 | return entity
15 | }
16 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/FIFOCache.swift:
--------------------------------------------------------------------------------
1 | /// A cache that removes mappings in FIFO order.
2 | struct FIFOCache where Key: Hashable {
3 | private var queue: CyclicDeque
4 | private var mappings: [Key: Value] = [:]
5 |
6 | init(capacity: Int = 16) {
7 | queue = CyclicDeque(capacity: capacity)
8 | }
9 |
10 | subscript(key: Key) -> Value? {
11 | mappings[key]
12 | }
13 |
14 | mutating func insert(_ key: Key, _ value: Value) {
15 | mappings[key] = value
16 | if let removed = queue.pushBack(key) {
17 | mappings[removed] = nil
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/ControlPadHUDNode.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | private func makeControlPadHUDStickNode(size: CGFloat) -> SKNode {
4 | let node = SKShapeNode(circleOfRadius: size * 2)
5 | node.lineWidth = 0
6 | node.fillColor = NodeConstants.foregroundColor.withAlphaComponent(0.8)
7 | node.userData = ["isControlPadStick": true]
8 | return node
9 | }
10 |
11 | func makeControlPadHUDNode(size: CGFloat) -> SKNode {
12 | let node = SKShapeNode(circleOfRadius: size * 2)
13 | node.lineWidth = 0
14 | node.fillColor = NodeConstants.overlayBackground
15 | node.addChild(makeControlPadHUDStickNode(size: size / 3))
16 | return node
17 | }
18 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | runs-on: macos-latest
12 | strategy:
13 | matrix:
14 | include:
15 | - scheme: MiniBlocks
16 | destination: generic/platform=macOS
17 | - scheme: MiniBlocksToGo
18 | destination: generic/platform=iOS
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Build ${{ matrix.scheme }}
23 | run: xcodebuild -project MiniBlocks/MiniBlocks.xcodeproj -scheme "${{ matrix.scheme }}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO
24 |
25 | # TODO: Run tests
26 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/HotbarHUDSlotNode.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | func makeHotbarHUDSlotNode(size: CGFloat, lineThickness: CGFloat) -> SKNode {
4 | let node = SKShapeNode(rect: CGRect(center: CGPoint(x: 0, y: 0), size: CGSize(width: size, height: size)))
5 | node.strokeColor = .black
6 | node.lineWidth = lineThickness
7 | node.lineJoin = .bevel
8 | node.fillColor = .black.withAlphaComponent(0.7)
9 | return node
10 | }
11 |
12 | func updateHotbarHUDSlotNode(_ node: SKNode, lineThickness: CGFloat?) {
13 | guard let node = node as? SKShapeNode else { return }
14 |
15 | if let lineThickness = lineThickness {
16 | node.lineWidth = lineThickness
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/ItemNode.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | private func loadSpriteTexture(for itemType: ItemType) -> SKTexture {
4 | let texture: SKTexture
5 |
6 | switch itemType {
7 | case .block(let blockType):
8 | guard let image = blockTextureImages[blockType] else { fatalError("No block texture for \(blockType)") }
9 | texture = SKTexture(image: image)
10 | }
11 |
12 | return texture
13 | }
14 |
15 | // TODO: Cache SKTextures?
16 |
17 | func makeItemNode(for item: Item, size: CGFloat) -> SKNode {
18 | let node = SKSpriteNode(texture: loadSpriteTexture(for: item.type))
19 | node.size = CGSize(width: size, height: size)
20 | return node
21 | }
22 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Wraparound.swift:
--------------------------------------------------------------------------------
1 | /// 'Wraps' the number around using modular arithemtic.
2 | @propertyWrapper
3 | struct Wraparound: Hashable where Value: SignedInteger {
4 | enum CodingKeys: String, CodingKey {
5 | case _wrappedValue = "value"
6 | case modulus
7 | }
8 |
9 | private var _wrappedValue: Value
10 | private let modulus: Value
11 |
12 | var wrappedValue: Value {
13 | get { _wrappedValue }
14 | set { _wrappedValue = newValue.floorMod(modulus) }
15 | }
16 |
17 | init(wrappedValue: Value, modulus: Value) {
18 | assert(modulus > 0)
19 | _wrappedValue = wrappedValue
20 | self.modulus = modulus
21 | }
22 | }
23 |
24 | extension Wraparound: Codable where Value: Codable {}
25 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // MiniBlocksToGo
4 | //
5 | // Created by Fredrik on 10.01.22.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 | var window: UIWindow?
12 |
13 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
14 | guard let scene = scene as? UIWindowScene else { return }
15 |
16 | // Set up window
17 | let window = UIWindow(windowScene: scene)
18 | window.rootViewController = MiniBlocksViewController(
19 | sceneFrame: scene.screen.bounds
20 | )
21 | self.window = window
22 | window.makeKeyAndVisible()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksTests/Utils/GridIterator2Tests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MiniBlocks
3 |
4 | class GridIterator2Tests: XCTestCase {
5 | func testIterator() {
6 | let topLeft = BlockPos2(x: 3, z: 6)
7 | let bottomRight = BlockPos2(x: 5, z: 10)
8 | let iterator = GridIterator2(topLeftInclusive: topLeft, bottomRightExclusive: bottomRight)
9 | let values = Array(iterator)
10 |
11 | XCTAssertEqual(values, [
12 | BlockPos2(x: 3, z: 6),
13 | BlockPos2(x: 4, z: 6),
14 | BlockPos2(x: 3, z: 7),
15 | BlockPos2(x: 4, z: 7),
16 | BlockPos2(x: 3, z: 8),
17 | BlockPos2(x: 4, z: 8),
18 | BlockPos2(x: 3, z: 9),
19 | BlockPos2(x: 4, z: 9),
20 | ])
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksTests/Utils/ThrottlerTests.swift:
--------------------------------------------------------------------------------
1 | @testable import MiniBlocks
2 | import XCTest
3 |
4 | class ThrottlerTests: XCTestCase {
5 | func testThrottler() {
6 | var counter = 0
7 | let action = { counter += 1 }
8 | var throttler = Throttler(interval: 1)
9 |
10 | throttler.submit(deltaTime: 0, action: action)
11 | XCTAssertEqual(counter, 1)
12 |
13 | throttler.submit(deltaTime: 0.2, action: action)
14 | throttler.submit(deltaTime: 0.4, action: action)
15 | XCTAssertEqual(counter, 1)
16 |
17 | throttler.submit(deltaTime: 0.5, action: action)
18 | XCTAssertEqual(counter, 2)
19 |
20 | throttler.submit(deltaTime: 1.2, action: action)
21 | XCTAssertEqual(counter, 3)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/TouchInteractable.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | /// Indicates that the implementing component may be controlled by touch.
4 | protocol TouchInteractable {
5 | func onTap(at point: CGPoint) -> Bool
6 |
7 | func shouldReceiveDrag(at point: CGPoint) -> Bool
8 |
9 | func onDragStart(at point: CGPoint)
10 |
11 | func onDragMove(by delta: CGVector, start: CGPoint, current: CGPoint)
12 |
13 | func onDragEnd()
14 | }
15 |
16 | extension TouchInteractable {
17 | func onTap(at point: CGPoint) -> Bool { false }
18 |
19 | func shouldReceiveDrag(at point: CGPoint) -> Bool { false }
20 |
21 | func onDragStart(at point: CGPoint) {}
22 |
23 | func onDragMove(by delta: CGVector, start: CGPoint, current: CGPoint) {}
24 |
25 | func onDragEnd() {}
26 | }
27 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/GridIterator2.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Iterates over a rectangle on the 2D grid.
4 | struct GridIterator2: IteratorProtocol, Sequence where Element: Vec2Protocol, Element.Coordinate: SignedInteger {
5 | let topLeftInclusive: Element
6 | let bottomRightExclusive: Element
7 | var i: Element.Coordinate = 0
8 |
9 | private var width: Element.Coordinate { bottomRightExclusive.x - topLeftInclusive.x }
10 | private var pos: Element { topLeftInclusive + Element(x: i % width, z: i / width) }
11 | private var isDone: Bool { pos.x >= bottomRightExclusive.x || pos.z >= bottomRightExclusive.z }
12 |
13 | mutating func next() -> Element? {
14 | guard !isDone else { return nil }
15 | let pos = pos
16 | i += 1
17 | return pos
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/HotbarHUDEntity.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | func makeHotbarHUDEntity(in frame: CGRect, playerEntity: GKEntity) -> GKEntity {
5 | // Create node
6 | let node = SKNode()
7 | node.position = CGPoint(x: frame.midX, y: frame.minY)
8 |
9 | // Create entity
10 | let entity = GKEntity()
11 | entity.addComponent(SpriteNodeComponent(node: node))
12 | entity.addComponent(PlayerAssociationComponent(playerEntity: playerEntity))
13 | entity.addComponent(HotbarHUDLoadComponent())
14 | entity.addComponent(HotbarHUDControlComponent())
15 |
16 | if let worldEntity = playerEntity.component(ofType: WorldAssociationComponent.self)?.worldEntity {
17 | entity.addComponent(WorldAssociationComponent(worldEntity: worldEntity))
18 | }
19 |
20 | return entity
21 | }
22 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/Strip.swift:
--------------------------------------------------------------------------------
1 | /// A vertical 1x1 'slice' of blocks.
2 | struct Strip: Hashable, Codable, Sequence {
3 | var blocks: [Int: Block] = [:]
4 |
5 | var isEmpty: Bool { blocks.isEmpty }
6 | var nilIfEmpty: Self? { isEmpty ? nil : self }
7 |
8 | var topmost: (y: Int, block: Block)? {
9 | block(below: nil)
10 | }
11 |
12 | subscript(y: Int) -> Block? {
13 | get { blocks[y] }
14 | set { blocks[y] = newValue }
15 | }
16 |
17 | func block(below y: Int?) -> (y: Int, Block)? {
18 | blocks
19 | .filter { $0.key <= (y ?? .max) }
20 | .max { $0.key < $1.key }
21 | .map { (y: $0.key, block: $0.value) }
22 | }
23 |
24 | func makeIterator() -> Dictionary.Iterator {
25 | blocks.makeIterator()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/ControlPadHUDEntity.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | func makeControlPadHUDEntity(in frame: CGRect, playerEntity: GKEntity, size: CGFloat = 40) -> GKEntity {
5 | // Create node
6 | let node = makeControlPadHUDNode(size: size)
7 | let offset = 2.8 * size
8 | node.position = CGPoint(x: frame.minX + offset, y: frame.minY + offset)
9 |
10 | // Create entity
11 | let entity = GKEntity()
12 | entity.addComponent(SpriteNodeComponent(node: node))
13 | entity.addComponent(PlayerAssociationComponent(playerEntity: playerEntity))
14 | entity.addComponent(ControlPadHUDControlComponent())
15 |
16 | if let worldEntity = playerEntity.component(ofType: WorldAssociationComponent.self)?.worldEntity {
17 | entity.addComponent(WorldAssociationComponent(worldEntity: worldEntity))
18 | }
19 |
20 | return entity
21 | }
22 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/WorldAssociationComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | /// Associates the entity with a world entity.
4 | class WorldAssociationComponent: GKComponent {
5 | let worldEntity: GKEntity
6 |
7 | var world: World? {
8 | get { worldEntity.component(ofType: WorldComponent.self)?.world }
9 | set { worldEntity.component(ofType: WorldComponent.self)?.world = newValue! }
10 | }
11 |
12 | var worldNode: SCNNode? {
13 | worldEntity.component(ofType: SceneNodeComponent.self)?.node
14 | }
15 |
16 | var worldLoadComponent: WorldLoadComponent? {
17 | worldEntity.component(ofType: WorldLoadComponent.self)
18 | }
19 |
20 | init(worldEntity: GKEntity) {
21 | self.worldEntity = worldEntity
22 | super.init()
23 | }
24 |
25 | required init?(coder: NSCoder) {
26 | nil
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/ChunkRegion.swift:
--------------------------------------------------------------------------------
1 | /// A rectangle of chunks in the world.
2 | struct ChunkRegion: Sequence {
3 | let topLeftInclusive: ChunkPos
4 | let bottomRightExclusive: ChunkPos
5 |
6 | init(topLeftInclusive: ChunkPos, bottomRightExclusive: ChunkPos) {
7 | self.topLeftInclusive = topLeftInclusive
8 | self.bottomRightExclusive = bottomRightExclusive
9 | }
10 |
11 | init(around center: ChunkPos, radius: Int) {
12 | let radiusVec = ChunkPos(x: radius, z: radius)
13 | self.init(
14 | topLeftInclusive: center - radiusVec,
15 | bottomRightExclusive: center + radiusVec
16 | )
17 | }
18 |
19 | func makeIterator() -> GridIterator2 {
20 | GridIterator2(
21 | topLeftInclusive: topLeftInclusive,
22 | bottomRightExclusive: bottomRightExclusive
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/MouseCaptureVisibilityComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | /// Makes the components visibility dependent on whether the mouse is captured.
4 | class MouseCaptureVisibilityComponent: GKComponent {
5 | /// If true, the associated node will be visible if and only if the mouse is captured.
6 | /// If false, the associated node will be visible if and only if it is not captured.
7 | private let visibleWhenCaptured: Bool
8 |
9 | private var node: SKNode? {
10 | entity?.component(ofType: SpriteNodeComponent.self)?.node
11 | }
12 |
13 | init(visibleWhenCaptured: Bool) {
14 | self.visibleWhenCaptured = visibleWhenCaptured
15 | super.init()
16 | }
17 |
18 | required init?(coder: NSCoder) {
19 | nil
20 | }
21 |
22 | func update(mouseCaptured: Bool) {
23 | node?.isHidden = visibleWhenCaptured != mouseCaptured
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/ChunkPos.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 |
3 | /// A flat position on the chunk grid.
4 | struct ChunkPos: Hashable, Codable, Sequence, Vec2Protocol {
5 | var x: Int
6 | var z: Int
7 |
8 | var topLeftInclusive: BlockPos2 {
9 | BlockPos2(x: x * ChunkConstants.size, z: z * ChunkConstants.size)
10 | }
11 |
12 | var bottomRightExclusive: BlockPos2 {
13 | BlockPos2(x: (x + 1) * ChunkConstants.size, z: (z + 1) * ChunkConstants.size)
14 | }
15 |
16 | init(containing pos: BlockPos2) {
17 | x = pos.x.floorDiv(ChunkConstants.size)
18 | z = pos.z.floorDiv(ChunkConstants.size)
19 | }
20 |
21 | init(x: Int, z: Int) {
22 | self.x = x
23 | self.z = z
24 | }
25 |
26 | func makeIterator() -> GridIterator2 {
27 | GridIterator2(topLeftInclusive: topLeftInclusive, bottomRightExclusive: bottomRightExclusive)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Throttler.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A facility that only executes an action if it hasn't executed within the last interval.
4 | struct Throttler {
5 | let interval: TimeInterval
6 | private var timeSinceLastRun: TimeInterval = .infinity
7 |
8 | init(interval: TimeInterval) {
9 | self.interval = interval
10 | }
11 |
12 | mutating func submit(deltaTime: TimeInterval, action: () -> Void, orElse alternativeAction: () -> Void = {}) {
13 | advance(deltaTime: deltaTime)
14 | if timeSinceLastRun > interval {
15 | action()
16 | timeSinceLastRun = 0
17 | } else {
18 | alternativeAction()
19 | }
20 | }
21 |
22 | mutating func advance(deltaTime: TimeInterval) {
23 | timeSinceLastRun += deltaTime
24 | }
25 |
26 | mutating func reset() {
27 | timeSinceLastRun = .infinity
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksTests/Utils/ChunkPosTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MiniBlocks
3 |
4 | class ChunkPosTests: XCTestCase {
5 | func testChunkPosContaining() {
6 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 0, z: 0)), ChunkPos(x: 0, z: 0))
7 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 15, z: 0)), ChunkPos(x: 0, z: 0))
8 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 15, z: 15)), ChunkPos(x: 0, z: 0))
9 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 16, z: 15)), ChunkPos(x: 1, z: 0))
10 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 16, z: -1)), ChunkPos(x: 1, z: -1))
11 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 16, z: -16)), ChunkPos(x: 1, z: -1))
12 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 16, z: -17)), ChunkPos(x: 1, z: -2))
13 | XCTAssertEqual(ChunkPos(containing: BlockPos2(x: 16, z: -32)), ChunkPos(x: 1, z: -2))
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/View/MiniBlocksSceneView.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 |
3 | /// A SceneKit view that fixes key handling when an overlayed SpriteKit scene is present. See https://developer.apple.com/library/archive/samplecode/Badger/Listings/Common_View_swift.html
4 | class MiniBlocksSceneView: SCNView {
5 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
6 | weak var keyEventsDelegate: NSResponder?
7 |
8 | override func keyDown(with event: NSEvent) {
9 | if let keyEventsDelegate = keyEventsDelegate {
10 | keyEventsDelegate.keyDown(with: event)
11 | } else {
12 | super.keyDown(with: event)
13 | }
14 | }
15 |
16 | override func keyUp(with event: NSEvent) {
17 | if let keyEventsDelegate = keyEventsDelegate {
18 | keyEventsDelegate.keyUp(with: event)
19 | } else {
20 | super.keyUp(with: event)
21 | }
22 | }
23 | #endif
24 | }
25 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/HotbarHUDControlComponent.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | /// Handles touches on the hotbar.
5 | class HotbarHUDControlComponent: GKComponent, TouchInteractable {
6 | private var node: SKNode? {
7 | entity?.component(ofType: SpriteNodeComponent.self)?.node
8 | }
9 |
10 | private var playerAssociationComponent: PlayerAssociationComponent? {
11 | entity?.component(ofType: PlayerAssociationComponent.self)
12 | }
13 |
14 | private var playerInfo: PlayerInfo? {
15 | get { playerAssociationComponent?.playerInfo }
16 | set { playerAssociationComponent?.playerInfo = newValue! }
17 | }
18 |
19 | func onTap(at point: CGPoint) -> Bool {
20 | guard let slotIndex = node?.scene?.nodes(at: point).compactMap({ $0.userData?["hotbarSlotIndex"] as? Int }).first else { return false }
21 | playerInfo?.selectedHotbarSlot = slotIndex
22 | playerInfo?.achieve(.hotbar)
23 | return true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/CompatibilityLayer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreGraphics
3 |
4 | /// A small compatibility layer for running the playground on different platforms
5 | /// like macOS and iOS while sharing a common codebase. This works, since AppKit
6 | /// and UIKit share many similarities, making it easy to abstract over the (few)
7 | /// differences.
8 |
9 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
10 |
11 | import AppKit
12 |
13 | public typealias Color = NSColor
14 | public typealias Image = NSImage
15 | public typealias ViewController = NSViewController
16 | public typealias SceneFloat = CGFloat
17 |
18 | /// Dummy protocol.
19 | public protocol GestureRecognizerDelegate {}
20 |
21 | #endif
22 |
23 | #if canImport(UIKit)
24 |
25 | import UIKit
26 |
27 | public typealias Color = UIColor
28 | public typealias Image = UIImage
29 | public typealias ViewController = UIViewController
30 | public typealias SceneFloat = Float
31 | public typealias GestureRecognizerDelegate = UIGestureRecognizerDelegate
32 |
33 | #endif
34 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/AchievementHUDEntity.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | func makeAchievementHUDEntity(
5 | in frame: CGRect,
6 | playerEntity: GKEntity,
7 | fontSize: CGFloat = 14,
8 | usesMouseKeyboardControls: Box
9 | ) -> GKEntity {
10 | // Create node
11 | let node = SKNode()
12 | node.position = CGPoint(x: frame.midX, y: frame.maxY - 20)
13 |
14 | // Create entity
15 | let entity = GKEntity()
16 | entity.addComponent(SpriteNodeComponent(node: node))
17 | entity.addComponent(PlayerAssociationComponent(playerEntity: playerEntity))
18 | entity.addComponent(MouseCaptureVisibilityComponent(visibleWhenCaptured: true))
19 | entity.addComponent(AchievementHUDLoadComponent(fontSize: fontSize, usesMouseKeyboardControls: usesMouseKeyboardControls))
20 |
21 | if let worldEntity = playerEntity.component(ofType: WorldAssociationComponent.self)?.worldEntity {
22 | entity.addComponent(WorldAssociationComponent(worldEntity: worldEntity))
23 | }
24 |
25 | return entity
26 | }
27 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/WorldEntity.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | func makeDemoBlockPositions() -> [BlockPos3] {
4 | let radius = 50
5 | return (-radius...radius).flatMap { x in
6 | (-radius...radius).map { z in
7 | BlockPos3(
8 | x: x,
9 | y: Int((-5 * sin(CGFloat(x) / 10) * cos(CGFloat(z) / 10)).rounded()),
10 | z: z
11 | )
12 | }
13 | }
14 | }
15 |
16 | func makeWorldEntity(world: World, retainSpawnChunks: Bool = false) -> GKEntity {
17 | // Create node
18 | let node = SCNNode()
19 |
20 | // Create entity
21 | let entity = GKEntity()
22 | entity.addComponent(WorldComponent(world: world))
23 | entity.addComponent(WorldAssociationComponent(worldEntity: entity)) // a world is associated with itself too
24 | entity.addComponent(SceneNodeComponent(node: node))
25 | entity.addComponent(WorldLoadComponent())
26 |
27 | if retainSpawnChunks {
28 | entity.addComponent(WorldRetainComponent())
29 | }
30 |
31 | return entity
32 | }
33 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/CoreGraphicsUtils.swift:
--------------------------------------------------------------------------------
1 | import CoreGraphics
2 |
3 | extension CGPoint {
4 | static func +(lhs: Self, rhs: Self) -> Self {
5 | Self(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
6 | }
7 |
8 | static func -(lhs: Self, rhs: Self) -> Self {
9 | Self(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
10 | }
11 |
12 | static func +(lhs: Self, rhs: CGSize) -> Self {
13 | Self(x: lhs.x + rhs.width, y: lhs.y + rhs.height)
14 | }
15 |
16 | static func -(lhs: Self, rhs: CGSize) -> Self {
17 | Self(x: lhs.x - rhs.width, y: lhs.y - rhs.height)
18 | }
19 | }
20 |
21 | extension CGSize {
22 | static func *(lhs: Self, rhs: CGFloat) -> Self {
23 | Self(width: lhs.width * rhs, height: lhs.height * rhs)
24 | }
25 |
26 | static func /(lhs: Self, rhs: CGFloat) -> Self {
27 | Self(width: lhs.width / rhs, height: lhs.height / rhs)
28 | }
29 | }
30 |
31 | extension CGRect {
32 | init(center: CGPoint, size: CGSize) {
33 | self.init(origin: center - (size / 2), size: size)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/DebugHUDEntity.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | func makeDebugHUDEntity(in frame: CGRect, playerEntity: GKEntity, fontSize: CGFloat = 15) -> GKEntity {
5 | // Create node
6 | let padding: CGFloat = 5
7 | let node = SKLabelNode()
8 | node.fontColor = .white
9 | node.fontName = NodeConstants.fontName
10 | node.fontSize = fontSize
11 | node.position = CGPoint(x: frame.minX + padding, y: frame.maxY - padding)
12 | node.numberOfLines = 0
13 | node.verticalAlignmentMode = .top
14 | node.horizontalAlignmentMode = .left
15 |
16 | // Create entity
17 | let entity = GKEntity()
18 | entity.addComponent(SpriteNodeComponent(node: node))
19 | entity.addComponent(PlayerAssociationComponent(playerEntity: playerEntity))
20 | entity.addComponent(DebugHUDLoadComponent())
21 |
22 | if let worldEntity = playerEntity.component(ofType: WorldAssociationComponent.self)?.worldEntity {
23 | entity.addComponent(WorldAssociationComponent(worldEntity: worldEntity))
24 | }
25 |
26 | return entity
27 | }
28 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/PauseHUDNode.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | private func makeBackgroundNode(size: CGSize) -> SKNode {
4 | let node = SKShapeNode(rect: CGRect(center: CGPoint(x: 0, y: 0), size: size))
5 | node.strokeColor = .clear
6 | node.lineWidth = 0
7 | node.fillColor = .black.withAlphaComponent(0.92)
8 | return node
9 | }
10 |
11 | private func makeLabelNode(text: String, offset: CGFloat = 0, fontSize: CGFloat) -> SKNode {
12 | let node = SKLabelNode(text: text)
13 | node.fontSize = fontSize
14 | node.fontColor = NodeConstants.foregroundColor
15 | node.fontName = NodeConstants.fontName
16 | node.verticalAlignmentMode = .center
17 | node.position = CGPoint(x: 0, y: offset)
18 | return node
19 | }
20 |
21 | func makePauseHUDNode(size: CGSize, fontSize: CGFloat) -> SKNode {
22 | let node = SKNode()
23 | node.addChild(makeBackgroundNode(size: size))
24 | node.addChild(makeLabelNode(text: "Click to capture mouse!", fontSize: fontSize))
25 | node.addChild(makeLabelNode(text: "(Press Esc to exit)", offset: -1.4 * fontSize, fontSize: fontSize * 0.8))
26 | return node
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/WorldGeneratorType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | private var cache = [WorldGeneratorType: WorldGenerator]()
4 |
5 | /// The type of world generator used.
6 | public enum WorldGeneratorType: Hashable, Codable, WorldGenerator {
7 | case empty
8 | case flat
9 | case wavyHills
10 | case ocean
11 | case nature(seed: String)
12 |
13 | var generator: WorldGenerator {
14 | if let generator = cache[self] {
15 | return generator
16 | } else {
17 | let generator: WorldGenerator
18 | switch self {
19 | case .empty: generator = EmptyWorldGenerator()
20 | case .flat: generator = FlatWorldGenerator()
21 | case .wavyHills: generator = WavyHillsWorldGenerator()
22 | case .ocean: generator = OceanWorldGenerator()
23 | case .nature(let seed): generator = NatureWorldGenerator(seed: seed)
24 | }
25 | cache[self] = generator
26 | return generator
27 | }
28 | }
29 |
30 | func generate(at pos: BlockPos2) -> Strip {
31 | generator.generate(at: pos)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksTests/Model/AchievementsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MiniBlocks
3 |
4 | class AchievementsTests: XCTestCase {
5 | func testAchievements() {
6 | let empty: Achievements = []
7 | XCTAssert(empty.isEmpty)
8 | XCTAssertFalse(empty.isSingle)
9 | XCTAssertEqual(Array(empty), [])
10 | XCTAssertEqual(empty.next, [])
11 |
12 | let single: Achievements = .moveAround
13 | XCTAssertFalse(single.isEmpty)
14 | XCTAssert(single.isSingle)
15 | XCTAssertEqual(Array(single), [single])
16 | XCTAssertEqual(single.next, .jump)
17 |
18 | let another: Achievements = .jump
19 | XCTAssertFalse(another.isEmpty)
20 | XCTAssert(another.isSingle)
21 | XCTAssertEqual(Array(another), [another])
22 | XCTAssertEqual(another.next, .sprint)
23 |
24 | let composite: Achievements = [.moveAround, .sprint]
25 | XCTAssertFalse(composite.isEmpty)
26 | XCTAssertFalse(composite.isSingle)
27 | XCTAssertEqual(Array(composite), [.moveAround, .sprint])
28 | XCTAssertEqual(composite.next, .jump)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksTests/Utils/MathUtilsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MiniBlocks
3 |
4 | class MathUtilsTests: XCTestCase {
5 | func testFloorDiv() {
6 | XCTAssertEqual(11.floorDiv(5), 2)
7 | XCTAssertEqual(5.floorDiv(5), 1)
8 | XCTAssertEqual(4.floorDiv(5), 0)
9 | XCTAssertEqual(2.floorDiv(5), 0)
10 | XCTAssertEqual(0.floorDiv(5), 0)
11 | XCTAssertEqual((-1).floorDiv(5), -1)
12 | XCTAssertEqual((-4).floorDiv(5), -1)
13 | XCTAssertEqual((-5).floorDiv(5), -1)
14 | XCTAssertEqual((-6).floorDiv(5), -2)
15 | XCTAssertEqual((-24).floorDiv(5), -5)
16 | XCTAssertEqual((-25).floorDiv(5), -5)
17 | XCTAssertEqual((-26).floorDiv(5), -6)
18 | }
19 |
20 | func testFloorMod() {
21 | XCTAssertEqual(5.floorMod(4), 1)
22 | XCTAssertEqual(4.floorMod(4), 0)
23 | XCTAssertEqual(3.floorMod(4), 3)
24 | XCTAssertEqual(0.floorMod(4), 0)
25 | XCTAssertEqual((-1).floorMod(4), 3)
26 | XCTAssertEqual((-2).floorMod(4), 2)
27 | XCTAssertEqual((-3).floorMod(4), 1)
28 | XCTAssertEqual((-4).floorMod(4), 0)
29 | XCTAssertEqual((-5).floorMod(4), 3)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Deque.swift:
--------------------------------------------------------------------------------
1 | /// A double-ended queue. The protocol makes no guarantee about the precise semantics of such queues, in particular implementations are free to e.g. remove elements at the front when new ones are inserted at the back.
2 | protocol Deque: Sequence {
3 | associatedtype Element
4 |
5 | /// The number of elements.
6 | var count: Int { get }
7 |
8 | /// Peeks at the front.
9 | var first: Element? { get }
10 |
11 | /// Peeks at the end.
12 | var last: Element? { get }
13 |
14 | /// Inserts an element at the beginning. May return a removed element.
15 | @discardableResult
16 | mutating func pushFront(_ element: Element) -> Element?
17 |
18 | /// Inserts an element at the end. May return a removed element.
19 | @discardableResult
20 | mutating func pushBack(_ element: Element) -> Element?
21 |
22 | /// Extracts an element at the beginning.
23 | @discardableResult
24 | mutating func popFront() -> Element?
25 |
26 | /// Extracts an element at the end.
27 | @discardableResult
28 | mutating func popBack() -> Element?
29 | }
30 |
31 | extension Deque {
32 | /// Whether the queue is empty.
33 | var isEmpty: Bool { count == 0 }
34 | }
35 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
2 |
3 | // WARNING:
4 | // This file is automatically generated.
5 | // Do not edit it by hand because the contents will be replaced.
6 |
7 | import PackageDescription
8 | import AppleProductTypes
9 |
10 | let package = Package(
11 | name: "MiniBlocks",
12 | platforms: [
13 | .iOS("15.2")
14 | ],
15 | products: [
16 | .iOSApplication(
17 | name: "MiniBlocks",
18 | targets: ["AppModule"],
19 | bundleIdentifier: "dev.fwcd.MiniBlocks",
20 | teamIdentifier: "SS8V8UGLY8",
21 | displayVersion: "1.0",
22 | bundleVersion: "1",
23 | iconAssetName: "AppIcon",
24 | accentColorAssetName: "AccentColor",
25 | supportedDeviceFamilies: [
26 | .pad,
27 | .phone
28 | ],
29 | supportedInterfaceOrientations: [
30 | .landscapeRight,
31 | .landscapeLeft
32 | ]
33 | )
34 | ],
35 | targets: [
36 | .executableTarget(
37 | name: "AppModule",
38 | path: ".",
39 | resources: [
40 | .process("Resources")
41 | ]
42 | )
43 | ]
44 | )
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/PlayerAssocationComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | /// Associates the associated entity with a player.
4 | class PlayerAssociationComponent: GKComponent {
5 | let playerEntity: GKEntity
6 |
7 | var playerName: String? {
8 | playerEntity.component(ofType: NameComponent.self)?.name
9 | }
10 |
11 | var lookAtBlockComponent: LookAtBlockComponent? {
12 | playerEntity.component(ofType: LookAtBlockComponent.self)
13 | }
14 |
15 | private var worldAssocationComponent: WorldAssociationComponent? {
16 | entity?.component(ofType: WorldAssociationComponent.self)
17 | }
18 |
19 | private var world: World? {
20 | get { worldAssocationComponent?.world }
21 | set { worldAssocationComponent?.world = newValue! }
22 | }
23 |
24 | var playerInfo: PlayerInfo? {
25 | get { playerName.flatMap { world?[playerInfoFor: $0] } }
26 | set {
27 | guard let playerName = playerName else { return }
28 | world?[playerInfoFor: playerName] = newValue!
29 | }
30 | }
31 |
32 | init(playerEntity: GKEntity) {
33 | self.playerEntity = playerEntity
34 | super.init()
35 | }
36 |
37 | required init?(coder: NSCoder) {
38 | nil
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/AchievementHUDNode.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | private func makeBackgroundNode(size: CGSize) -> SKNode {
4 | let node = SKShapeNode(rect: CGRect(center: CGPoint(x: 0, y: 0), size: size))
5 | node.strokeColor = .clear
6 | node.lineWidth = 0
7 | node.fillColor = NodeConstants.overlayBackground
8 | return node
9 | }
10 |
11 | private func makeLabelNode(text: String, offset: CGFloat = 0, fontSize: CGFloat) -> SKNode {
12 | let node = SKLabelNode(text: text)
13 | node.fontSize = fontSize
14 | node.fontColor = NodeConstants.foregroundColor
15 | node.fontName = NodeConstants.fontName
16 | node.verticalAlignmentMode = .center
17 | node.position = CGPoint(x: 0, y: offset)
18 | return node
19 | }
20 |
21 | func makeAchievementHUDNode(for achievement: Achievements, forMouseKeyboardControls: Bool, fontSize: CGFloat, padding: CGFloat = 10) -> SKNode? {
22 | guard let text = achievement.text(forMouseKeyboardControls: forMouseKeyboardControls) else { return nil }
23 | let node = SKNode()
24 | let label = makeLabelNode(text: text, fontSize: fontSize)
25 | let labelSize = label.frame.size
26 | node.addChild(makeBackgroundNode(size: CGSize(width: labelSize.width + padding, height: labelSize.height + padding)))
27 | node.addChild(label)
28 | return node
29 | }
30 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Node/BlockNode.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 |
3 | /// The texture mappings for every block type.
4 | let blockTextureImages: [BlockType: Image] = [
5 | .grass: Image(named: "TextureGrass.png")!,
6 | .sand: Image(named: "TextureSand.png")!,
7 | .stone: Image(named: "TextureStone.png")!,
8 | .water: Image(named: "TextureWater.png")!,
9 | .wood: Image(named: "TextureWood.png")!,
10 | .leaves: Image(named: "TextureLeaves.png")!,
11 | .bedrock: Image(named: "TextureBedrock.png")!,
12 | ]
13 |
14 | private func loadMaterial(for blockType: BlockType) -> SCNMaterial {
15 | let material = SCNMaterial()
16 | material.diffuse.contents = blockTextureImages[blockType]
17 | material.diffuse.minificationFilter = .none
18 | material.diffuse.magnificationFilter = .none
19 | return material
20 | }
21 |
22 | private func loadGeometry(for blockType: BlockType) -> SCNGeometry {
23 | let box = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
24 | box.materials = [loadMaterial(for: blockType)]
25 | return box
26 | }
27 |
28 | private let geometries: [BlockType: SCNGeometry] = Dictionary(
29 | uniqueKeysWithValues: blockTextureImages.keys.map { ($0, loadGeometry(for: $0)) }
30 | )
31 |
32 | func makeBlockNode(for block: Block) -> SCNNode {
33 | SCNNode(geometry: geometries[block.type])
34 | }
35 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksTests/Utils/DebouncerTests.swift:
--------------------------------------------------------------------------------
1 | @testable import MiniBlocks
2 | import XCTest
3 |
4 | class DebouncerTests: XCTestCase {
5 | func testDebouncer() {
6 | var counter = 0
7 | let action = { counter += 1 }
8 | var debouncer = Debouncer(interval: 1)
9 |
10 | debouncer.submit(deltaTime: 0, defer: true, action: action)
11 | XCTAssertEqual(counter, 0)
12 |
13 | debouncer.submit(deltaTime: 0.2, defer: true, action: action)
14 | debouncer.submit(deltaTime: 0.4, defer: true, action: action)
15 | debouncer.submit(deltaTime: 0.8, defer: true, action: action)
16 | debouncer.submit(deltaTime: 0.3, defer: true, action: action)
17 | XCTAssertEqual(counter, 0)
18 |
19 | debouncer.submit(deltaTime: 1.2, defer: false, action: action)
20 | XCTAssertEqual(counter, 1)
21 |
22 | debouncer.submit(deltaTime: 1.4, defer: false, action: action)
23 | XCTAssertEqual(counter, 1)
24 |
25 | debouncer.submit(deltaTime: 1.4, defer: true, action: action)
26 | XCTAssertEqual(counter, 1)
27 |
28 | debouncer.submit(deltaTime: 1.2, defer: false, action: action)
29 | XCTAssertEqual(counter, 2)
30 |
31 | debouncer.submit(deltaTime: 0.3, defer: true, force: true, action: action)
32 | XCTAssertEqual(counter, 3)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/PlayerPositioningComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 | import SceneKit
3 |
4 | /// Positions the node according to the position and velocity from the associated player info.
5 | class PlayerPositioningComponent: GKComponent {
6 | private var throttler = Throttler(interval: 0.05)
7 |
8 | private var node: SCNNode? {
9 | entity?.component(ofType: SceneNodeComponent.self)?.node
10 | }
11 |
12 | private var heightOffset: Vec3 {
13 | entity?.component(ofType: HeightAboveGroundComponent.self)?.offset ?? .zero
14 | }
15 |
16 | private var playerAssociationComponent: PlayerAssociationComponent? {
17 | entity?.component(ofType: PlayerAssociationComponent.self)
18 | }
19 |
20 | private var playerInfo: PlayerInfo? {
21 | get { playerAssociationComponent?.playerInfo }
22 | set { playerAssociationComponent?.playerInfo = newValue! }
23 | }
24 |
25 | override func update(deltaTime seconds: TimeInterval) {
26 | guard let node = node,
27 | var playerInfo = playerInfo else { return }
28 |
29 | let interval = throttler.interval
30 | throttler.submit(deltaTime: seconds) {
31 | node.runAction(.move(to: SCNVector3(playerInfo.position - heightOffset), duration: interval))
32 | playerInfo.applyVelocity()
33 | self.playerInfo = playerInfo
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // MiniBlocks
4 | //
5 | // Created by Fredrik on 06.01.22.
6 | //
7 |
8 | import Cocoa
9 |
10 | class AppDelegate: NSObject, NSApplicationDelegate {
11 | func applicationDidFinishLaunching(_ aNotification: Notification) {
12 | // Set up menu
13 | let appMenu = NSMenu()
14 | appMenu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate), keyEquivalent: "q")
15 | let appMenuItem = NSMenuItem()
16 | appMenuItem.submenu = appMenu
17 | let menu = NSMenu()
18 | menu.addItem(appMenuItem)
19 | NSApp.menu = menu
20 |
21 | // Set up window
22 | let width = 800
23 | let height = 600
24 | let window = NSWindow(
25 | contentRect: NSRect(x: 0, y: 0, width: width, height: height),
26 | styleMask: [.miniaturizable, .closable, .titled, .resizable],
27 | backing: .buffered,
28 | defer: false
29 | )
30 | window.center()
31 | window.title = "MiniBlocks"
32 | window.contentViewController = MiniBlocksViewController(
33 | sceneFrame: CGRect(x: 0, y: 0, width: width, height: height),
34 | worldGenerator: .nature(seed: "default"),
35 | gameMode: .survival,
36 | renderDistance: 12,
37 | debugStatsShown: false
38 | )
39 | window.makeKeyAndOrderFront(nil)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Debouncer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A facility that only executes an action after some time has passed. Relies on an update loop (such as the one provided by SceneKit) that repeatedly calls update.
4 | struct Debouncer {
5 | let interval: TimeInterval
6 | private var state: State = .idling
7 |
8 | private enum State {
9 | case idling
10 | case deferring(timeSinceLastRun: TimeInterval)
11 | }
12 |
13 | init(interval: TimeInterval) {
14 | self.interval = interval
15 | }
16 |
17 | mutating func submit(deltaTime: TimeInterval, defer: Bool, force: Bool = false, action: () -> Void) {
18 | if force {
19 | action()
20 | state = .idling
21 | } else {
22 | switch state {
23 | case .idling:
24 | if `defer` {
25 | state = .deferring(timeSinceLastRun: 0)
26 | }
27 | case .deferring(timeSinceLastRun: var timeSinceLastRun):
28 | timeSinceLastRun += deltaTime
29 | if `defer` {
30 | state = .deferring(timeSinceLastRun: 0)
31 | } else if timeSinceLastRun <= interval {
32 | state = .deferring(timeSinceLastRun: timeSinceLastRun)
33 | } else {
34 | action()
35 | state = .idling
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "32.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "64.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "256.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "512.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/LookAtBlockComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | /// Makes the associated node as being capable of looking at blocks (which will be highlighted accordingly).
4 | class LookAtBlockComponent: GKComponent {
5 | private let reachDistance: SceneFloat = 10
6 | private var lastHit: SCNNode? = nil
7 |
8 | /// The looked at block pos.
9 | private(set) var blockPos: BlockPos3? = nil
10 | /// The block pos for a new block.
11 | private(set) var blockPlacePos: BlockPos3? = nil
12 |
13 | private var node: SCNNode? {
14 | entity?.component(ofType: SceneNodeComponent.self)?.node
15 | }
16 |
17 | private var worldNode: SCNNode? {
18 | entity?.component(ofType: WorldAssociationComponent.self)?.worldNode
19 | }
20 |
21 | override func update(deltaTime seconds: TimeInterval) {
22 | guard let node = node,
23 | let parent = node.parent,
24 | let worldNode = worldNode else { return }
25 |
26 | // Find the node the player looks at and (for demo purposes) lower its opacity
27 |
28 | lastHit?.filters = nil
29 |
30 | let pos = parent.convertPosition(node.position, to: worldNode)
31 | let facing = parent.convertVector(node.worldFront, to: worldNode)
32 | let hits = worldNode.hitTestWithSegment(from: pos, to: pos + facing * reachDistance)
33 | let hit = hits.first
34 |
35 | if let filter = CIFilter(name: "CIColorControls") {
36 | filter.setValue(-0.05, forKey: kCIInputBrightnessKey)
37 | hit?.node.filters = [filter]
38 | }
39 |
40 | blockPos = hit.map { BlockPos3(rounding: $0.node.position) }
41 | blockPlacePos = hit.map { BlockPos3(rounding: $0.node.position + $0.worldNormal) }
42 | lastHit = hit?.node
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // MiniBlocksToGo
4 | //
5 | // Created by Fredrik on 10.01.22.
6 | //
7 |
8 | import UIKit
9 |
10 | @main
11 | class AppDelegate: UIResponder, UIApplicationDelegate {
12 | var window: UIWindow?
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | return true
17 | }
18 |
19 | func applicationWillResignActive(_ application: UIApplication) {
20 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
21 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
22 | }
23 |
24 | func applicationDidEnterBackground(_ application: UIApplication) {
25 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
26 | }
27 |
28 | func applicationWillEnterForeground(_ application: UIApplication) {
29 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
30 | }
31 |
32 | func applicationDidBecomeActive(_ application: UIApplication) {
33 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/View/KeyCode.swift:
--------------------------------------------------------------------------------
1 | /// Cocoa key codes for convenience, based on https://stackoverflow.com/questions/36900825/where-are-all-the-cocoa-keycodes.
2 | struct KeyCode: RawRepresentable, Hashable {
3 | static let a = Self(rawValue: 0)
4 | static let s = Self(rawValue: 1)
5 | static let d = Self(rawValue: 2)
6 | static let w = Self(rawValue: 13)
7 | static let one = Self(rawValue: 18)
8 | static let two = Self(rawValue: 19)
9 | static let three = Self(rawValue: 20)
10 | static let four = Self(rawValue: 21)
11 | static let five = Self(rawValue: 23)
12 | static let six = Self(rawValue: 22)
13 | static let seven = Self(rawValue: 26)
14 | static let eight = Self(rawValue: 28)
15 | static let nine = Self(rawValue: 25)
16 | static let zero = Self(rawValue: 29)
17 | static let space = Self(rawValue: 49)
18 | static let escape = Self(rawValue: 53)
19 | static let arrowLeft = Self(rawValue: 123)
20 | static let arrowRight = Self(rawValue: 124)
21 | static let arrowDown = Self(rawValue: 125)
22 | static let arrowUp = Self(rawValue: 126)
23 | static let f1 = Self(rawValue: 122)
24 | static let f2 = Self(rawValue: 120)
25 | static let f3 = Self(rawValue: 99)
26 | static let f4 = Self(rawValue: 118)
27 | static let f5 = Self(rawValue: 96)
28 | static let f6 = Self(rawValue: 97)
29 | static let f7 = Self(rawValue: 98)
30 | static let f8 = Self(rawValue: 100)
31 | static let f9 = Self(rawValue: 101)
32 |
33 | let rawValue: UInt16
34 |
35 | var numericValue: Int? {
36 | switch self {
37 | case .zero: return 0
38 | case .one: return 1
39 | case .two: return 2
40 | case .three: return 3
41 | case .four: return 4
42 | case .five: return 5
43 | case .six: return 6
44 | case .seven: return 7
45 | case .eight: return 8
46 | case .nine: return 9
47 | default: return nil
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Entity/PlayerEntity.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 | import SceneKit
3 |
4 | func makePlayerEntity(
5 | name: String,
6 | position: Vec3,
7 | gameMode: GameMode,
8 | worldEntity: GKEntity,
9 | retainRadius: Int,
10 | ambientOcclusionEnabled: Bool,
11 | handShown: Bool
12 | ) -> GKEntity {
13 | // Create node
14 | let height: Double = 1.5
15 | let camera = SCNCamera()
16 | camera.zNear = 0.1
17 | if ambientOcclusionEnabled {
18 | camera.screenSpaceAmbientOcclusionIntensity = 0.5
19 | camera.screenSpaceAmbientOcclusionRadius = 0.5
20 | }
21 | let handNode = SCNNode()
22 | let node = SCNNode()
23 | node.camera = camera
24 | node.addChildNode(handNode)
25 |
26 | // Create entity
27 | let entity = GKEntity()
28 | entity.addComponent(NameComponent(name: name))
29 | entity.addComponent(WorldAssociationComponent(worldEntity: worldEntity))
30 | entity.addComponent(WorldRetainComponent(retainRadius: retainRadius)) // players retain chunks around themselves
31 | entity.addComponent(SceneNodeComponent(node: node))
32 | entity.addComponent(PlayerControlComponent())
33 | entity.addComponent(PlayerPositioningComponent())
34 | entity.addComponent(HeightAboveGroundComponent(heightAboveGround: height))
35 | entity.addComponent(PlayerGravityComponent())
36 | entity.addComponent(LookAtBlockComponent())
37 | entity.addComponent(PlayerAssociationComponent(playerEntity: entity)) // a player is associated with itself too
38 |
39 | if handShown {
40 | entity.addComponent(HandNodeComponent(node: handNode))
41 | entity.addComponent(HandLoadComponent())
42 | }
43 |
44 | // Set initial player info
45 | if let component = worldEntity.component(ofType: WorldComponent.self) {
46 | var playerInfo = component.world[playerInfoFor: name]
47 |
48 | playerInfo.position = position
49 | playerInfo.gameMode = gameMode
50 |
51 | component.world[playerInfoFor: name] = playerInfo
52 | }
53 |
54 | return entity
55 | }
56 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/PlayerInfo.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 |
3 | /// Information about a player.
4 | struct PlayerInfo: Codable, Hashable {
5 | /// The player's main inventory.
6 | var inventory: Inventory = Inventory(slotCount: InventoryConstants.inventorySlotCount)
7 | /// The player's hotbar.
8 | var hotbar: Inventory = Inventory(slotCount: InventoryConstants.hotbarSlotCount)
9 | /// The hotbar slot index which is currently active.
10 | @Wraparound(modulus: InventoryConstants.hotbarSlotCount)
11 | var selectedHotbarSlot: Int = 0
12 | /// The position of the player.
13 | var position: Vec3 = .zero
14 | /// The velocity of the player.
15 | var velocity: Vec3 = .zero
16 | /// Whether the player is on the ground.
17 | var isOnGround: Bool = false
18 | /// Whether the player is about to leave the ground (e.g. due to a jump).
19 | var leavesGround: Bool = false
20 | /// The game mode the player is in.
21 | var gameMode: GameMode = .creative
22 | /// Whether the player has the debug overlay enabled.
23 | var hasDebugHUDEnabled: Bool = false
24 | /// Achievements by the player.
25 | private(set) var achievements: Achievements = []
26 |
27 | /// The currently selected stack on the hotbar.
28 | var selectedHotbarStack: ItemStack? {
29 | get { hotbar[selectedHotbarSlot] }
30 | set { hotbar[selectedHotbarSlot] = newValue }
31 | }
32 |
33 | init() {
34 | // TODO: This is only for debugging purposes
35 | hotbar[0] = ItemStack(item: Item(type: .block(.grass)))
36 | hotbar[1] = ItemStack(item: Item(type: .block(.sand)))
37 | hotbar[2] = ItemStack(item: Item(type: .block(.stone)))
38 | hotbar[3] = ItemStack(item: Item(type: .block(.wood)))
39 | hotbar[4] = ItemStack(item: Item(type: .block(.leaves)))
40 | }
41 |
42 | mutating func applyVelocity() {
43 | position += velocity
44 | }
45 |
46 | mutating func achieve(_ newAchievements: Achievements) {
47 | achievements.formUnion(newAchievements.intersection(achievements.next))
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MiniBlocks
2 |
3 | [](https://github.com/fwcd/mini-blocks/actions/workflows/build.yml)
4 |
5 | A procedurally generated open-world sandbox game for macOS and iOS, inspired by Minecraft.
6 |
7 | 
8 |
9 | ## Description
10 |
11 | MiniBlocks is an open-world sandbox game, inspired by Minecraft. The player is spawned in a three-dimensional voxel world and can interact with it by placing or breaking blocks with the mouse. The world is infinite and new chunks are generated procedurally as the player moves through the world.
12 |
13 | World generation is provided through a variety of world generators. The most interesting one - NatureWorldGenerator - incorporates techniques such as perlin noise maps and pseudo-randomness to first generate a height map and then 'decorate' it with structures such as trees.
14 |
15 | World loading happens in chunked deltas, i.e. only the new/obsolete chunks are added to/removed from the scene as the player moves.
16 |
17 | On the technical side the game uses the SceneKit framework for rendering and is structured using a combination of the entity-component architecture provided by GameplayKit and classic model-view separation. This choice of architecture makes it extensible, avoiding common pitfalls in traditional inheritance-heavy designs, and provides great modularity on top.
18 |
19 | By including a compatibility layer, the game can use either AppKit, GameController or UIKit for input handling, thereby making it possible to play the game on macOS, iOS and iPadOS with mouse, keyboard or touch controls.
20 |
21 | The game and assets are created entirely from scratch and © fwcd 2022.
22 |
23 | ## Repository Structure
24 |
25 | The code is located [in the playground app project](MiniBlocks.swiftpm) to comply with the Swift Student Challenge's requirements. For convenience, an Xcode project along with actual macOS/iOS targets and unit tests is provided too, however.
26 |
27 | ## See also
28 |
29 | * [MiniCut](https://github.com/fwcd/mini-cut), a tiny video editor built with SpriteKit (my 2021 project)
30 | * [MiniJam](https://github.com/fwcd/mini-jam), a tiny digital audio workstation built with SwiftUI (my 2020 project)
31 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/HandLoadComponent.swift:
--------------------------------------------------------------------------------
1 | import SceneKit
2 | import GameplayKit
3 |
4 | /// Renders the first-person hand (with the held item) for a player.
5 | class HandLoadComponent: GKComponent {
6 | /// The item last rendered.
7 | private var lastItem: Item?
8 |
9 | private var node: SCNNode? {
10 | entity?.component(ofType: HandNodeComponent.self)?.node
11 | }
12 |
13 | private var world: World? {
14 | get { entity?.component(ofType: WorldAssociationComponent.self)?.world }
15 | set { entity?.component(ofType: WorldAssociationComponent.self)?.world = newValue }
16 | }
17 |
18 | private var playerInfo: PlayerInfo? {
19 | get { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo }
20 | set { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo = newValue }
21 | }
22 |
23 | private var item: Item? {
24 | playerInfo?.selectedHotbarStack?.item
25 | }
26 |
27 | override func update(deltaTime seconds: TimeInterval) {
28 | guard let node = node,
29 | lastItem != item else { return }
30 |
31 | for child in node.childNodes {
32 | child.removeFromParentNode()
33 | }
34 |
35 | if let item = item {
36 | switch item.type {
37 | case .block(let blockType):
38 | let block = makeBlockNode(for: Block(type: blockType))
39 | block.position = SCNVector3(x: 1.5, y: -1, z: -2.5)
40 | node.addChildNode(block)
41 | }
42 | }
43 |
44 | lastItem = item
45 | }
46 |
47 | /// Schedules a 'swing' animation indicating that e.g. a block was placed/broken.
48 | func swing() {
49 | let halfDuration = 0.15
50 | node?.runAction(.sequence([
51 | .group([
52 | .move(by: SCNVector3(x: 0, y: 0, z: -2), duration: halfDuration),
53 | .rotateBy(x: 0, y: 1, z: 1, duration: halfDuration),
54 | ]),
55 | .group([
56 | .move(to: SCNVector3(x: 0, y: 0, z: 0), duration: halfDuration),
57 | .rotateTo(x: 0, y: 0, z: 0, duration: halfDuration),
58 | ]),
59 | ]))
60 | }
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/PlayerGravityComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 | import SceneKit
3 |
4 | /// Accelerates the associated node downwards (i.e. in negative-y direction).
5 | class PlayerGravityComponent: GKComponent {
6 | var acceleration: Double = -0.4
7 | private var throttler = Throttler(interval: 0.1)
8 |
9 | private var world: World? {
10 | entity?.component(ofType: WorldAssociationComponent.self)?.world
11 | }
12 |
13 | private var playerAssociationComponent: PlayerAssociationComponent? {
14 | entity?.component(ofType: PlayerAssociationComponent.self)
15 | }
16 |
17 | private var playerInfo: PlayerInfo? {
18 | get { playerAssociationComponent?.playerInfo }
19 | set { playerAssociationComponent?.playerInfo = newValue! }
20 | }
21 |
22 | override func update(deltaTime seconds: TimeInterval) {
23 | // Note that we don't use the if-var-and-assign idiom for playerInfo due to responsiveness issues (and inout bindings aren't in Swift yet)
24 | guard let world = world,
25 | playerInfo != nil,
26 | playerInfo!.gameMode.enablesGravityAndCollisions else { return }
27 |
28 | throttler.submit(deltaTime: seconds) {
29 | // Fetch position and velocity
30 | var position = playerInfo!.position
31 | var velocity = playerInfo!.velocity
32 |
33 | let y = position.y
34 | let yBound = world.height(below: BlockPos3(rounding: position)).map { $0 + 1 }
35 |
36 | let willBeOnGround = !playerInfo!.leavesGround && yBound.map { y + velocity.y <= Double($0) } ?? false
37 |
38 | if willBeOnGround {
39 | velocity.y = 0
40 | if !playerInfo!.isOnGround {
41 | // We are reaching the ground, correct the position
42 | position.y = Double(yBound!)
43 | }
44 | } else {
45 | // We are airborne, apply gravity
46 | velocity.y += acceleration
47 | }
48 |
49 | playerInfo!.isOnGround = willBeOnGround
50 | playerInfo!.leavesGround = false
51 |
52 | playerInfo!.position = position
53 | playerInfo!.velocity = velocity
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/DebugHUDLoadComponent.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | class DebugHUDLoadComponent: GKComponent {
5 | private var lastEnabled: Bool = false
6 |
7 | private var node: SKLabelNode? {
8 | entity?.component(ofType: SpriteNodeComponent.self)?.node as? SKLabelNode
9 | }
10 |
11 | private var world: World? {
12 | get { entity?.component(ofType: WorldAssociationComponent.self)?.world }
13 | set { entity?.component(ofType: WorldAssociationComponent.self)?.world = newValue }
14 | }
15 |
16 | private var playerInfo: PlayerInfo? {
17 | get { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo }
18 | set { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo = newValue }
19 | }
20 |
21 | private var lookAtBlockComponent: LookAtBlockComponent? {
22 | entity?.component(ofType: PlayerAssociationComponent.self)?.lookAtBlockComponent
23 | }
24 |
25 | override func update(deltaTime seconds: TimeInterval) {
26 | guard let playerInfo = playerInfo,
27 | let node = node else { return }
28 |
29 | let isEnabled = playerInfo.hasDebugHUDEnabled
30 |
31 | if isEnabled {
32 | DispatchQueue.main.async { [self] in
33 | var stats = [
34 | ("Position", format(pos: playerInfo.position)),
35 | ("Block Position", format(pos: BlockPos3(rounding: playerInfo.position))),
36 | ("Game Mode", "\(playerInfo.gameMode)"),
37 | ]
38 |
39 | if let component = lookAtBlockComponent {
40 | stats += [
41 | ("Looking At", component.blockPos.map(format(pos:)) ?? "nil"),
42 | ("Placing At", component.blockPlacePos.map(format(pos:)) ?? "nil"),
43 | ]
44 | }
45 |
46 | node.text = stats.map { "\($0.0): \($0.1)" }.joined(separator: "\n")
47 | }
48 | } else if lastEnabled {
49 | node.text = nil
50 | }
51 |
52 | lastEnabled = isEnabled
53 | }
54 |
55 | private func format(pos: BlockPos3) -> String {
56 | "x \(pos.x), y \(pos.y), z \(pos.z)"
57 | }
58 |
59 | private func format(pos: Vec3) -> String {
60 | String(format: "x %.4f, y %.4f, z %.4f", pos.x, pos.y, pos.z)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/ControlPadHUDControlComponent.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | /// Handles control pad touches.
5 | class ControlPadHUDControlComponent: GKComponent, TouchInteractable {
6 | private var node: SKNode? {
7 | entity?.component(ofType: SpriteNodeComponent.self)?.node
8 | }
9 |
10 | private var stickNode: SKNode? {
11 | node?.children.filter({ ($0.userData?["isControlPadStick"] as? Bool) ?? false }).first
12 | }
13 |
14 | private var playerAssociationComponent: PlayerAssociationComponent? {
15 | entity?.component(ofType: PlayerAssociationComponent.self)
16 | }
17 |
18 | private var playerControlComponent: PlayerControlComponent? {
19 | playerAssociationComponent?.playerEntity.component(ofType: PlayerControlComponent.self)
20 | }
21 |
22 | func onTap(at point: CGPoint) -> Bool {
23 | guard let node = node,
24 | let parent = node.parent,
25 | let scene = node.scene,
26 | node.contains(scene.convert(point, to: parent)) else { return false }
27 | playerControlComponent?.jump()
28 | return true
29 | }
30 |
31 | func shouldReceiveDrag(at point: CGPoint) -> Bool {
32 | guard let node = node,
33 | let parent = node.parent,
34 | let scene = node.scene else { return false }
35 | return node.contains(scene.convert(point, to: parent))
36 | }
37 |
38 | func onDragMove(by delta: CGVector, start: CGPoint, current: CGPoint) {
39 | guard let stickNode = stickNode else { return }
40 | let offset = (current - start)
41 | let rawBaseVelocity = Vec3(x: offset.x, y: 0, z: -offset.y)
42 | let shouldSprint = rawBaseVelocity.length > stickNode.frame.width * 2
43 | let baseVelocity = rawBaseVelocity.normalized
44 |
45 | DispatchQueue.main.async {
46 | stickNode.position = offset
47 | }
48 |
49 | if shouldSprint {
50 | playerControlComponent?.add(motionInput: .sprint)
51 | } else {
52 | playerControlComponent?.remove(motionInput: .sprint)
53 | }
54 | playerControlComponent?.requestedBaseVelocity = baseVelocity
55 | }
56 |
57 | func onDragEnd() {
58 | if let stickNode = stickNode {
59 | DispatchQueue.main.async {
60 | stickNode.position = CGPoint(x: 0, y: 0)
61 | }
62 | }
63 | playerControlComponent?.remove(motionInput: .sprint)
64 | playerControlComponent?.requestedBaseVelocity = .zero
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Vec2Protocol.swift:
--------------------------------------------------------------------------------
1 | /// A 2D vector.
2 | protocol Vec2Protocol {
3 | associatedtype Coordinate: SignedNumeric
4 |
5 | var x: Coordinate { get set }
6 | var z: Coordinate { get set }
7 |
8 | init(x: Coordinate, z: Coordinate)
9 | }
10 |
11 | extension Vec2Protocol {
12 | static var zero: Self { Self() }
13 |
14 | var squaredLength: Coordinate {
15 | (x * x) + (z * z)
16 | }
17 |
18 | var neighbors: [Self] {
19 | [
20 | self + Self(x: 1),
21 | self - Self(x: 1),
22 | self + Self(z: 1),
23 | self - Self(z: 1)
24 | ]
25 | }
26 |
27 | init() {
28 | self.init(x: 0, z: 0)
29 | }
30 |
31 | init(x: Coordinate) {
32 | self.init(x: x, z: 0)
33 | }
34 |
35 | init(z: Coordinate) {
36 | self.init(x: 0, z: z)
37 | }
38 |
39 | static func +(lhs: Self, rhs: Self) -> Self {
40 | Self(x: lhs.x + rhs.x, z: lhs.z + rhs.z)
41 | }
42 |
43 | static func -(lhs: Self, rhs: Self) -> Self {
44 | Self(x: lhs.x - rhs.x, z: lhs.z - rhs.z)
45 | }
46 |
47 | static func +=(lhs: inout Self, rhs: Self) {
48 | lhs.x += rhs.x
49 | lhs.z += rhs.z
50 | }
51 |
52 | static func -=(lhs: inout Self, rhs: Self) {
53 | lhs.x -= rhs.x
54 | lhs.z -= rhs.z
55 | }
56 |
57 | static prefix func -(rhs: Self) -> Self {
58 | Self(x: -rhs.x, z: -rhs.z)
59 | }
60 |
61 | static func *(lhs: Self, rhs: Coordinate) -> Self {
62 | Self(x: lhs.x * rhs, z: lhs.z * rhs)
63 | }
64 |
65 | static func *(lhs: Coordinate, rhs: Self) -> Self {
66 | rhs * lhs
67 | }
68 |
69 | static func *=(lhs: inout Self, rhs: Coordinate) {
70 | lhs.x *= rhs
71 | lhs.z *= rhs
72 | }
73 |
74 | func dot(_ rhs: Self) -> Coordinate {
75 | x * rhs.x + z * rhs.z
76 | }
77 | }
78 |
79 | extension Vec2Protocol where Coordinate: FloatingPoint {
80 | static func /(lhs: Self, rhs: Coordinate) -> Self {
81 | Self(x: lhs.x / rhs, z: lhs.z / rhs)
82 | }
83 |
84 | static func /=(lhs: inout Self, rhs: Coordinate) {
85 | lhs.x /= rhs
86 | lhs.z /= rhs
87 | }
88 | }
89 |
90 | extension Vec2Protocol where Coordinate: SignedInteger {
91 | static func /(lhs: Self, rhs: Coordinate) -> Self {
92 | Self(x: lhs.x / rhs, z: lhs.z / rhs)
93 | }
94 |
95 | static func /=(lhs: inout Self, rhs: Coordinate) {
96 | lhs.x /= rhs
97 | lhs.z /= rhs
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksTests/Utils/CyclicDequeTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import MiniBlocks
3 |
4 | class CyclicDequeTests: XCTestCase {
5 | func testCyclicDeque() {
6 | var a = CyclicDeque(capacity: 3)
7 | XCTAssert(a.isEmpty)
8 | XCTAssert(!a.isFull)
9 | XCTAssertNil(a.first)
10 | XCTAssertNil(a.last)
11 | XCTAssertEqual(Array(a), [])
12 |
13 | a.pushBack(23)
14 | XCTAssert(!a.isFull)
15 | XCTAssertEqual(a.count, 1)
16 | XCTAssertEqual(a.first, 23)
17 | XCTAssertEqual(a.last, 23)
18 | XCTAssertEqual(Array(a), [23])
19 |
20 | a.pushBack(43)
21 | a.pushBack(59)
22 | XCTAssert(a.isFull)
23 | XCTAssertEqual(a.count, 3)
24 | XCTAssertEqual(a.first, 23)
25 | XCTAssertEqual(a.last, 59)
26 | XCTAssertEqual(Array(a), [23, 43, 59])
27 |
28 | a.pushBack(98)
29 | XCTAssert(a.isFull)
30 | XCTAssertEqual(a.count, 3)
31 | XCTAssertEqual(a.first, 43)
32 | XCTAssertEqual(a.last, 98)
33 | XCTAssertEqual(Array(a), [43, 59, 98])
34 |
35 | XCTAssertEqual(a.popFront(), 43)
36 | XCTAssert(!a.isFull)
37 | XCTAssertEqual(Array(a), [59, 98])
38 | XCTAssertEqual(a.popBack(), 98)
39 | XCTAssertEqual(a.popFront(), 59)
40 | XCTAssert(a.isEmpty)
41 | XCTAssertNil(a.popFront())
42 | XCTAssertNil(a.popFront())
43 |
44 | var b = CyclicDeque(capacity: 2)
45 |
46 | XCTAssertNil(b.pushFront(1))
47 | XCTAssertEqual(b.count, 1)
48 |
49 | XCTAssertNil(b.pushFront(2))
50 | XCTAssertEqual(b.pushFront(9), 1)
51 | XCTAssertEqual(b.pushFront(7), 2)
52 | XCTAssertEqual(b.count, 2)
53 | XCTAssertEqual(Array(b), [7, 9])
54 |
55 | XCTAssertEqual(b.pushBack(12), 7)
56 | XCTAssertEqual(b.count, 2)
57 | XCTAssertEqual(Array(b), [9, 12])
58 |
59 | XCTAssertEqual(b.popFront(), 9)
60 | XCTAssertEqual(b.count, 1)
61 | XCTAssertEqual(Array(b), [12])
62 |
63 | XCTAssertNil(b.pushFront(98))
64 | XCTAssertEqual(b.count, 2)
65 | XCTAssertEqual(Array(b), [98, 12])
66 |
67 | XCTAssertEqual(b.popBack(), 12)
68 | XCTAssertEqual(b.count, 1)
69 | XCTAssertEqual(Array(b), [98])
70 |
71 | XCTAssertNil(b.pushBack(70))
72 | XCTAssertEqual(b.count, 2)
73 | XCTAssertEqual(Array(b), [98, 70])
74 |
75 | XCTAssertEqual(b.pushBack(34), 98)
76 | XCTAssertEqual(b.count, 2)
77 | XCTAssertEqual(Array(b), [70, 34])
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/AchievementHUDLoadComponent.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | /// Renders the next achievement for a player from the world model to a SpriteKit node.
5 | class AchievementHUDLoadComponent: GKComponent {
6 | /// The achievement last rendered to a SpriteKit node.
7 | private var lastAchievement: Achievements?
8 | /// The last mouse/keyboard status for detecting changes.
9 | private var lastUsedMouseKeyboardControls: Bool
10 |
11 | private let fontSize: CGFloat
12 | private let lineThickness: CGFloat = 4
13 | @Box private var usesMouseKeyboardControls: Bool
14 |
15 | private var node: SKNode? {
16 | entity?.component(ofType: SpriteNodeComponent.self)?.node
17 | }
18 |
19 | private var world: World? {
20 | get { entity?.component(ofType: WorldAssociationComponent.self)?.world }
21 | set { entity?.component(ofType: WorldAssociationComponent.self)?.world = newValue }
22 | }
23 |
24 | private var playerInfo: PlayerInfo? {
25 | get { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo }
26 | set { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo = newValue }
27 | }
28 |
29 | private var achievements: Achievements? {
30 | playerInfo?.achievements
31 | }
32 |
33 | private var nextAchievement: Achievements? {
34 | achievements?.next
35 | }
36 |
37 | init(fontSize: CGFloat, usesMouseKeyboardControls: Box) {
38 | self.fontSize = fontSize
39 | _usesMouseKeyboardControls = usesMouseKeyboardControls
40 | lastUsedMouseKeyboardControls = usesMouseKeyboardControls.wrappedValue
41 | super.init()
42 | }
43 |
44 | required init?(coder: NSCoder) {
45 | nil
46 | }
47 |
48 | override func update(deltaTime seconds: TimeInterval) {
49 | guard let node = node else { return }
50 |
51 | if lastAchievement != nextAchievement || lastUsedMouseKeyboardControls != usesMouseKeyboardControls {
52 | DispatchQueue.main.async { [self] in
53 | // Update when achievements change
54 | node.removeAllChildren()
55 |
56 | if let nextAchievement = nextAchievement,
57 | let achievementNode = makeAchievementHUDNode(for: nextAchievement, forMouseKeyboardControls: usesMouseKeyboardControls, fontSize: 13) {
58 | node.addChild(achievementNode)
59 | }
60 |
61 | lastAchievement = nextAchievement
62 | lastUsedMouseKeyboardControls = usesMouseKeyboardControls
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "58.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "87.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "80.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "120.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "180.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "40.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "58.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "40.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "80.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "idiom" : "ipad",
89 | "scale" : "1x",
90 | "size" : "76x76"
91 | },
92 | {
93 | "filename" : "152.png",
94 | "idiom" : "ipad",
95 | "scale" : "2x",
96 | "size" : "76x76"
97 | },
98 | {
99 | "filename" : "167.png",
100 | "idiom" : "ipad",
101 | "scale" : "2x",
102 | "size" : "83.5x83.5"
103 | },
104 | {
105 | "filename" : "1024.png",
106 | "idiom" : "ios-marketing",
107 | "scale" : "1x",
108 | "size" : "1024x1024"
109 | }
110 | ],
111 | "info" : {
112 | "author" : "xcode",
113 | "version" : 1
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "58.png",
17 | "idiom" : "iphone",
18 | "scale" : "2x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "87.png",
23 | "idiom" : "iphone",
24 | "scale" : "3x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "80.png",
29 | "idiom" : "iphone",
30 | "scale" : "2x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "filename" : "120.png",
35 | "idiom" : "iphone",
36 | "scale" : "3x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120.png",
41 | "idiom" : "iphone",
42 | "scale" : "2x",
43 | "size" : "60x60"
44 | },
45 | {
46 | "filename" : "180.png",
47 | "idiom" : "iphone",
48 | "scale" : "3x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "20.png",
53 | "idiom" : "ipad",
54 | "scale" : "1x",
55 | "size" : "20x20"
56 | },
57 | {
58 | "filename" : "40.png",
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "29.png",
65 | "idiom" : "ipad",
66 | "scale" : "1x",
67 | "size" : "29x29"
68 | },
69 | {
70 | "filename" : "58.png",
71 | "idiom" : "ipad",
72 | "scale" : "2x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "40.png",
77 | "idiom" : "ipad",
78 | "scale" : "1x",
79 | "size" : "40x40"
80 | },
81 | {
82 | "filename" : "80.png",
83 | "idiom" : "ipad",
84 | "scale" : "2x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "idiom" : "ipad",
89 | "scale" : "1x",
90 | "size" : "76x76"
91 | },
92 | {
93 | "filename" : "152.png",
94 | "idiom" : "ipad",
95 | "scale" : "2x",
96 | "size" : "76x76"
97 | },
98 | {
99 | "filename" : "167.png",
100 | "idiom" : "ipad",
101 | "scale" : "2x",
102 | "size" : "83.5x83.5"
103 | },
104 | {
105 | "filename" : "1024.png",
106 | "idiom" : "ios-marketing",
107 | "scale" : "1x",
108 | "size" : "1024x1024"
109 | }
110 | ],
111 | "info" : {
112 | "author" : "xcode",
113 | "version" : 1
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/WorldRetainComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 | import OSLog
3 |
4 | private let log = Logger(subsystem: "MiniBlocks", category: "WorldRetainComponent")
5 |
6 | /// Lets the associated node keep the world's chunks retained at its position.
7 | class WorldRetainComponent: GKComponent {
8 | private var previous: Set = []
9 | private var throttler = Throttler(interval: 0.2)
10 |
11 | /// Number of chunks to retain in each direction. Note that although we call it a 'radius', a square grid of chunks is loaded.
12 | var retainRadius: Int
13 |
14 | /// Number of chunks which the player may 'stray' from the lastUpdatePos until an update to the retained chunks is triggered.
15 | private var skipUpdateRadius: Int { min(2, retainRadius - 1) }
16 |
17 | private var lastUpdatePos: ChunkPos? = nil
18 |
19 | private var worldLoadComponent: WorldLoadComponent? {
20 | entity?.component(ofType: WorldAssociationComponent.self)?.worldLoadComponent
21 | }
22 |
23 | private var node: SCNNode? {
24 | entity?.component(ofType: SceneNodeComponent.self)?.node
25 | }
26 |
27 | private var currentPos: ChunkPos? {
28 | node.map { ChunkPos(containing: BlockPos3(rounding: $0.position).asVec2) }
29 | }
30 |
31 | private var shouldUpdate: Bool {
32 | guard let lastUpdatePos = lastUpdatePos,
33 | let currentPos = currentPos else { return true }
34 | return (currentPos - lastUpdatePos).squaredLength > skipUpdateRadius * skipUpdateRadius
35 | }
36 |
37 | init(retainRadius: Int = 6) {
38 | self.retainRadius = retainRadius
39 | super.init()
40 | }
41 |
42 | required init?(coder: NSCoder) {
43 | nil
44 | }
45 |
46 | override func update(deltaTime seconds: TimeInterval) {
47 | guard let worldLoadComponent = worldLoadComponent,
48 | let centerPos = currentPos else { return }
49 |
50 | throttler.submit(deltaTime: seconds) {
51 | guard shouldUpdate else { return }
52 | log.info("Updating retained chunks...")
53 |
54 | // TODO: Do delta updates here?
55 |
56 | // Release previous chunks
57 | for pos in previous {
58 | worldLoadComponent.releaseChunk(at: pos)
59 | }
60 |
61 | previous = []
62 |
63 | // Retain new chunks
64 | for pos in ChunkRegion(around: centerPos, radius: retainRadius) {
65 | worldLoadComponent.retainChunk(at: pos)
66 | previous.insert(pos)
67 | }
68 |
69 | lastUpdatePos = centerPos
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/NatureWorldGenerator.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | /// Generates a world with realistic terrain and natural features.
4 | struct NatureWorldGenerator: WorldGenerator {
5 | private let heightNoise: GKNoise
6 |
7 | var amplitude: Float = 8
8 | var scale: Float = 80
9 | var sandLevel: Int = 0
10 | var waterLevel: Int = -1
11 | var bedrockLevel: Int = -8
12 |
13 | var treeBaseHeight: Int = 5
14 | var leavesBaseHeight: Int = 2
15 |
16 | init(seed: String) {
17 | let noiseSeed: Int32 = seed.utf8.reduce(0) { ($0 << 1) ^ Int32($1) }
18 |
19 | heightNoise = GKNoise(GKPerlinNoiseSource(
20 | frequency: 1.2,
21 | octaveCount: 12,
22 | persistence: 0.8,
23 | lacunarity: 1.2,
24 | seed: noiseSeed
25 | ))
26 | }
27 |
28 | func generate(at pos: BlockPos2) -> Strip {
29 | var blocks: [Int: Block]
30 |
31 | // Generate terrain
32 | let y = terrainHeight(at: pos)
33 | if y > sandLevel {
34 | blocks = [y: Block(type: .grass)]
35 | } else if y >= waterLevel {
36 | blocks = [y: Block(type: .sand)]
37 | } else {
38 | blocks = Dictionary(uniqueKeysWithValues: (y...waterLevel).map { ($0, Block(type: .water)) })
39 | }
40 |
41 | // Generate ground below terrain
42 | if bedrockLevel + 1 < y {
43 | for i in (bedrockLevel + 1).. Int {
66 | Int(amplitude * heightNoise.value(atPosition: vectorOf(pos: pos)))
67 | }
68 |
69 | private func treeHeight(at pos: BlockPos2) -> Int {
70 | treeBaseHeight + min(3, ((((pos.x) << 1) % 10) ^ ((pos.z << 4) % 9)) % 4)
71 | }
72 |
73 | private func leavesHeight(at pos: BlockPos2) -> Int {
74 | max(0, leavesBaseHeight + (pos.x ^ pos.z) % 2)
75 | }
76 |
77 | private func isTree(at pos: BlockPos2) -> Bool {
78 | guard terrainHeight(at: pos) > sandLevel else { return false }
79 | let x = ((pos.x % 30) << 1) ^ pos.z % 100
80 | return x == 23
81 | }
82 |
83 | private func vectorOf(pos: BlockPos2) -> vector_float2 {
84 | vector_float2(x: Float(pos.x) / scale, y: Float(pos.z) / scale)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/HotbarHUDLoadComponent.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import GameplayKit
3 |
4 | /// Renders the hotbar for a player from the world model to a SpriteKit node.
5 | class HotbarHUDLoadComponent: GKComponent {
6 | /// The inventory last rendered to a SpriteKit node.
7 | private var lastInventory: Inventory?
8 | /// The slot selection last rendered to a SpriteKit node.
9 | private var lastSelectedHotbarSlot: Int?
10 |
11 | private var selectedSlotLineThickness: CGFloat = 4
12 | private var normalSlotLineThickness: CGFloat = 2
13 |
14 | private var node: SKNode? {
15 | entity?.component(ofType: SpriteNodeComponent.self)?.node
16 | }
17 |
18 | private var world: World? {
19 | get { entity?.component(ofType: WorldAssociationComponent.self)?.world }
20 | set { entity?.component(ofType: WorldAssociationComponent.self)?.world = newValue }
21 | }
22 |
23 | private var playerInfo: PlayerInfo? {
24 | get { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo }
25 | set { entity?.component(ofType: PlayerAssociationComponent.self)?.playerInfo = newValue }
26 | }
27 |
28 | private var inventory: Inventory? {
29 | get { playerInfo?.hotbar }
30 | set { playerInfo?.hotbar = newValue! }
31 | }
32 |
33 | override func update(deltaTime seconds: TimeInterval) {
34 | guard let node = node else { return }
35 |
36 | if inventory != lastInventory {
37 | // Redraw slots as inventory has changed
38 | DispatchQueue.main.async { [self] in
39 | // TODO: Delta updates?
40 | node.removeAllChildren()
41 |
42 | if let inventory = inventory {
43 | let slotSize: CGFloat = 40
44 | let itemSize: CGFloat = slotSize * 0.8
45 | let width = CGFloat(inventory.slotCount) * slotSize
46 |
47 | for i in 0.. CGFloat {
74 | playerInfo?.selectedHotbarSlot == i ? selectedSlotLineThickness : normalSlotLineThickness
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/MiniBlocks/MiniBlocks.xcodeproj/xcshareddata/xcschemes/MiniBlocks.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
55 |
61 |
62 |
63 |
64 |
70 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/Achievements.swift:
--------------------------------------------------------------------------------
1 | /// Represents in-game progress milestones by the player.
2 | struct Achievements: OptionSet, Sequence, Hashable, Codable {
3 | let rawValue: UInt64
4 |
5 | static let peekAround = Self(rawValue: 1 << 0)
6 | static let moveAround = Self(rawValue: 1 << 1)
7 | static let jump = Self(rawValue: 1 << 2)
8 | static let sprint = Self(rawValue: 1 << 3)
9 | static let hotbar = Self(rawValue: 1 << 4)
10 | static let useBlock = Self(rawValue: 1 << 5)
11 | static let breakBlock = Self(rawValue: 1 << 6)
12 |
13 | /// The achievements a new player is tasked with.
14 | static let root = peekAround
15 |
16 | /// Whether this is a single achivement.
17 | var isSingle: Bool {
18 | rawValue > 0 && (rawValue & (rawValue - 1)) == 0
19 | }
20 |
21 | /// The next achivements.
22 | var next: Achievements {
23 | if self == [] {
24 | return .root
25 | }
26 | guard isSingle else {
27 | return flatMap(\.next)
28 | .filter { !contains($0) }
29 | .reduce([]) { $0.union($1) }
30 | }
31 |
32 | switch self {
33 | case .peekAround: return .moveAround
34 | case .moveAround: return .jump
35 | case .jump: return .sprint
36 | case .sprint: return .hotbar
37 | case .hotbar: return .useBlock
38 | case .useBlock: return .breakBlock
39 | default: return []
40 | }
41 | }
42 |
43 | /// The user-facing text for a single achievement.
44 | func text(forMouseKeyboardControls: Bool) -> String? {
45 | if forMouseKeyboardControls {
46 | switch self {
47 | case .peekAround: return "Peek around by moving your mouse."
48 | case .moveAround: return "Move around using your WASD keys."
49 | case .jump: return "Press SPACE to jump."
50 | case .sprint: return "Hold SHIFT while moving around with WASD to sprint."
51 | case .hotbar: return "Scroll or press number keys to switch the held item."
52 | case .useBlock: return "Place a block by clicking/holding your right mouse button."
53 | case .breakBlock: return "Break a block by clicking/holding your left mouse button."
54 | default: return nil
55 | }
56 | } else {
57 | switch self {
58 | case .peekAround: return "Peek around by panning the screen (or by moving a connected mouse)."
59 | case .moveAround: return "Move around by dragging the control pad (or with your WASD keys)."
60 | case .jump: return "Tap the control pad (or press SPACE) to jump."
61 | case .sprint: return "Drag the control pad further to sprint (or hold SHIFT during WASD)."
62 | case .hotbar: return "Tap a hotbar slot (or press a number key) to select it."
63 | case .useBlock: return "Tap to place a block."
64 | case .breakBlock: return "Break a block by holding your finger (or your left mouse button)."
65 | default: return nil
66 | }
67 | }
68 | }
69 |
70 | func makeIterator() -> Iterator {
71 | Iterator(rawValue: rawValue)
72 | }
73 |
74 | struct Iterator: IteratorProtocol {
75 | let rawValue: RawValue
76 | var i: RawValue = 0
77 |
78 | mutating func next() -> Achievements? {
79 | while i < RawValue.bitWidth {
80 | let bitIndex = i
81 | i += 1
82 | let bit = (rawValue >> bitIndex) & 1 == 1
83 | if bit {
84 | return Achievements(rawValue: 1 << bitIndex)
85 | }
86 | }
87 | return nil
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/CyclicDeque.swift:
--------------------------------------------------------------------------------
1 | /// A fixed-size, ring buffer implementation of a double-ended queue.
2 | struct CyclicDeque: Deque {
3 | /// The array storing the elements.
4 | private var elements: [Element?]
5 | /// Index of the first element.
6 | private var front: Int = 0
7 | /// Index of one element past the last element, i.e. the next insertion position.
8 | private var back: Int = 0
9 |
10 | /// The maximum size of the queue.
11 | let capacity: Int
12 | /// The current size of the queue.
13 | private(set) var count: Int = 0
14 | /// Whether the queue has reached its maximum size.
15 | var isFull: Bool { count == capacity }
16 |
17 | var first: Element? {
18 | guard count > 0 else { return nil }
19 | return elements[front]
20 | }
21 |
22 | var last: Element? {
23 | guard count > 0 else { return nil }
24 | return elements[(back - 1).floorMod(capacity)]
25 | }
26 |
27 | init(capacity: Int) {
28 | self.capacity = capacity
29 | elements = Array(repeating: nil, count: capacity)
30 | }
31 |
32 | private mutating func incrementCountIfNeeded() {
33 | if count < capacity {
34 | count += 1
35 | }
36 | }
37 |
38 | private mutating func decrementCountIfNeeded() {
39 | if count > 0 {
40 | count -= 1
41 | }
42 | }
43 |
44 | /// Inserts an element at the front. O(1).
45 | @discardableResult
46 | mutating func pushFront(_ element: Element) -> Element? {
47 | let nextFront = (front - 1).floorMod(capacity)
48 | var removedElement: Element? = nil
49 | if isFull {
50 | assert(front == back)
51 | removedElement = elements[nextFront]
52 | back = nextFront
53 | }
54 | elements[nextFront] = element
55 | front = nextFront
56 | incrementCountIfNeeded()
57 | return removedElement
58 | }
59 |
60 | /// Inserts an element at the back. O(1).
61 | @discardableResult
62 | mutating func pushBack(_ element: Element) -> Element? {
63 | let nextBack = (back + 1) % capacity
64 | var removedElement: Element? = nil
65 | if isFull {
66 | assert(front == back)
67 | removedElement = elements[back]
68 | front = nextBack
69 | }
70 | elements[back] = element
71 | back = nextBack
72 | incrementCountIfNeeded()
73 | return removedElement
74 | }
75 |
76 | /// Extracts an element at the front. O(1).
77 | @discardableResult
78 | mutating func popFront() -> Element? {
79 | guard count > 0 else { return nil }
80 | let element = elements[front]
81 | elements[front] = nil
82 | front = (front + 1) % capacity
83 | decrementCountIfNeeded()
84 | return element
85 | }
86 |
87 | /// Extracts an element at the back. O(1).
88 | @discardableResult
89 | mutating func popBack() -> Element? {
90 | guard count > 0 else { return nil }
91 | let nextBack = (back - 1).floorMod(capacity)
92 | let element = elements[nextBack]
93 | elements[nextBack] = nil
94 | back = nextBack
95 | decrementCountIfNeeded()
96 | return element
97 | }
98 |
99 | func makeIterator() -> Iterator {
100 | Iterator(deque: self)
101 | }
102 |
103 | struct Iterator: IteratorProtocol {
104 | let deque: CyclicDeque
105 | var i: Int = 0
106 |
107 | mutating func next() -> Element? {
108 | guard i < deque.count else { return nil }
109 | let element: Element = deque.elements[(deque.front + i) % deque.capacity]!
110 | i += 1
111 | return element
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Model/World.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A model of the world.
4 | struct World: Codable, Sequence {
5 | enum CodingKeys: String, CodingKey {
6 | case generator
7 | case storedStrips = "strips"
8 | case playerInfos
9 | }
10 |
11 | /// The procedural generator that generates new strips.
12 | let generator: WorldGeneratorType
13 |
14 | /// The user-changed strips (which are to be saved).
15 | private(set) var storedStrips: [BlockPos2: Strip] = [:]
16 |
17 | /// The cached strips.
18 | @Box private var cachedStrips: [BlockPos2: Strip] = [:]
19 |
20 | /// Information about the players, keyed by username.
21 | private(set) var playerInfos: [String: PlayerInfo] = [:]
22 |
23 | /// Fetches the strip at the given position.
24 | subscript(_ pos: BlockPos2) -> Strip {
25 | get {
26 | if let strip = storedStrips[pos] ?? cachedStrips[pos] {
27 | return strip
28 | } else {
29 | let strip = generator.generate(at: pos)
30 | cachedStrips[pos] = strip
31 | return strip
32 | }
33 | }
34 | set {
35 | uncache(at: pos)
36 | storedStrips[pos] = newValue
37 | }
38 | }
39 |
40 | /// Fetches the player info for the given player name.
41 | subscript(playerInfoFor name: String) -> PlayerInfo {
42 | get { playerInfos[name] ?? PlayerInfo() }
43 | set { playerInfos[name] = newValue }
44 | }
45 |
46 | /// Removes the given strip from the cache. Doesn't change anything about the world semantically. O(1).
47 | func uncache(at pos: BlockPos2) {
48 | cachedStrips[pos] = nil
49 | }
50 |
51 | /// Creates an iterator over the strips.
52 | func makeIterator() -> Dictionary.Iterator {
53 | storedStrips.makeIterator()
54 | }
55 |
56 | /// Fetches the height the given position. O(n) where n is the number of blocks in the strip at the given (x, z) coordinates.
57 | func height(at pos: BlockPos2) -> Int? {
58 | self[pos].topmost?.y
59 | }
60 |
61 | /// Fetches the height below the given position. O(n) where n is the number of blocks in the strip at the given (x, z) coordinates.
62 | func height(below pos: BlockPos3) -> Int? {
63 | self[pos.asVec2].block(below: pos.y)?.y
64 | }
65 |
66 | /// Fetches the block at the given position. O(1).
67 | func block(at pos: BlockPos3) -> Block? {
68 | self[pos.asVec2][pos.y]
69 | }
70 |
71 | /// Checks whether there is a block at the given position. O(1).
72 | func hasBlock(at pos: BlockPos3) -> Bool {
73 | block(at: pos) != nil
74 | }
75 |
76 | /// Checks whether the block at the given position is fully occluded by others. O(1).
77 | func isOccluded(at pos: BlockPos3) -> Bool {
78 | eachNeighbor(at: pos) {
79 | block(at: $0).map { $0.type.isOpaque || $0.type.isLiquid } ?? false
80 | }
81 | }
82 |
83 | /// Checks whether something applies to all neighbors. We use this instead of e.g. reducing over the neighbor array for performance as many such checks (e.g. isOccluded) are on the hot path.
84 | func eachNeighbor(at pos: BlockPos3, satisfies predicate: (BlockPos3) -> Bool) -> Bool {
85 | predicate(pos + BlockPos3(x: 1))
86 | && predicate(pos - BlockPos3(x: 1))
87 | && predicate(pos + BlockPos3(y: 1))
88 | && predicate(pos - BlockPos3(y: 1))
89 | && predicate(pos + BlockPos3(z: 1))
90 | && predicate(pos - BlockPos3(z: 1))
91 | }
92 |
93 | /// Place the given block at the given position. O(1).
94 | mutating func place(block: Block?, at pos: BlockPos3) {
95 | self[pos.asVec2][pos.y] = block
96 | }
97 |
98 | /// Removes the block at the given position. O(1).
99 | mutating func breakBlock(at pos: BlockPos3) {
100 | place(block: nil, at: pos)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Utils/Vec3Protocol.swift:
--------------------------------------------------------------------------------
1 | /// A 3D vector.
2 | protocol Vec3Protocol {
3 | associatedtype Coordinate: SignedNumeric
4 |
5 | var x: Coordinate { get set }
6 | var y: Coordinate { get set }
7 | var z: Coordinate { get set }
8 |
9 | init(x: Coordinate, y: Coordinate, z: Coordinate)
10 | }
11 |
12 | extension Vec3Protocol {
13 | static var zero: Self { Self() }
14 |
15 | init() {
16 | self.init(x: 0, y: 0, z: 0)
17 | }
18 |
19 | init(x: Coordinate) {
20 | self.init(x: x, y: 0, z: 0)
21 | }
22 |
23 | init(y: Coordinate) {
24 | self.init(x: 0, y: y, z: 0)
25 | }
26 |
27 | init(z: Coordinate) {
28 | self.init(x: 0, y: 0, z: z)
29 | }
30 |
31 | var neighbors: [Self] {
32 | [
33 | self + Self(x: 1),
34 | self - Self(x: 1),
35 | self + Self(y: 1),
36 | self - Self(y: 1),
37 | self + Self(z: 1),
38 | self - Self(z: 1)
39 | ]
40 | }
41 |
42 | static func +(lhs: Self, rhs: Self) -> Self {
43 | Self(x: lhs.x + rhs.x, y: lhs.y + rhs.y, z: lhs.z + rhs.z)
44 | }
45 |
46 | static func -(lhs: Self, rhs: Self) -> Self {
47 | Self(x: lhs.x - rhs.x, y: lhs.y - rhs.y, z: lhs.z - rhs.z)
48 | }
49 |
50 | static func +=(lhs: inout Self, rhs: Self) {
51 | lhs.x += rhs.x
52 | lhs.y += rhs.y
53 | lhs.z += rhs.z
54 | }
55 |
56 | static func -=(lhs: inout Self, rhs: Self) {
57 | lhs.x -= rhs.x
58 | lhs.y -= rhs.y
59 | lhs.z -= rhs.z
60 | }
61 |
62 | static prefix func -(rhs: Self) -> Self {
63 | Self(x: -rhs.x, y: -rhs.y, z: -rhs.z)
64 | }
65 |
66 | static func *(lhs: Self, rhs: Coordinate) -> Self {
67 | Self(x: lhs.x * rhs, y: lhs.y * rhs, z: lhs.z * rhs)
68 | }
69 |
70 | static func *(lhs: Coordinate, rhs: Self) -> Self {
71 | rhs * lhs
72 | }
73 |
74 | static func *=(lhs: inout Self, rhs: Coordinate) {
75 | lhs.x *= rhs
76 | lhs.y *= rhs
77 | lhs.z *= rhs
78 | }
79 |
80 | func dot(_ rhs: Self) -> Coordinate {
81 | x * rhs.x + y * rhs.y + z * rhs.z
82 | }
83 | }
84 |
85 | extension Vec3Protocol where Coordinate: FloatingPoint {
86 | var length: Coordinate {
87 | (x * x + y * y + z * z).squareRoot()
88 | }
89 |
90 | var manhattanLength: Coordinate {
91 | abs(x) + abs(y)
92 | }
93 |
94 | var normalized: Self {
95 | let length = self.length
96 | return length > 0 ? self / length : self
97 | }
98 |
99 | init(_ other: V) where V: Vec3Protocol, V.Coordinate: BinaryInteger {
100 | self.init(
101 | x: Coordinate(other.x),
102 | y: Coordinate(other.y),
103 | z: Coordinate(other.z)
104 | )
105 | }
106 |
107 | func manhattanDistance(to rhs: Self) -> Coordinate {
108 | (self - rhs).manhattanLength
109 | }
110 |
111 | static func /(lhs: Self, rhs: Coordinate) -> Self {
112 | Self(x: lhs.x / rhs, y: lhs.y / rhs, z: lhs.z / rhs)
113 | }
114 |
115 | static func /=(lhs: inout Self, rhs: Coordinate) {
116 | lhs.x /= rhs
117 | lhs.y /= rhs
118 | lhs.z /= rhs
119 | }
120 |
121 | mutating func normalize() {
122 | self /= length
123 | }
124 | }
125 |
126 | extension Vec3Protocol where Coordinate: SignedInteger {
127 | static func /(lhs: Self, rhs: Coordinate) -> Self {
128 | Self(x: lhs.x / rhs, y: lhs.y / rhs, z: lhs.z / rhs)
129 | }
130 |
131 | static func /=(lhs: inout Self, rhs: Coordinate) {
132 | lhs.x /= rhs
133 | lhs.y /= rhs
134 | lhs.z /= rhs
135 | }
136 | }
137 |
138 | extension Vec3Protocol where Coordinate: BinaryFloatingPoint {
139 | init(_ other: V) where V: Vec3Protocol, V.Coordinate: BinaryFloatingPoint {
140 | self.init(
141 | x: Coordinate(other.x),
142 | y: Coordinate(other.y),
143 | z: Coordinate(other.z)
144 | )
145 | }
146 | }
147 |
148 | extension Vec3Protocol where Coordinate: BinaryInteger {
149 | init(rounding other: V) where V: Vec3Protocol, V.Coordinate: BinaryFloatingPoint {
150 | self.init(
151 | x: Coordinate(other.x.rounded()),
152 | y: Coordinate(other.y.rounded()),
153 | z: Coordinate(other.z.rounded())
154 | )
155 | }
156 |
157 | init(_ other: V) where V: Vec3Protocol, V.Coordinate: BinaryInteger {
158 | self.init(
159 | x: Coordinate(other.x),
160 | y: Coordinate(other.y),
161 | z: Coordinate(other.z)
162 | )
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/WorldLoadComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 | import SceneKit
3 | import OSLog
4 |
5 | private let log = Logger(subsystem: "MiniBlocks", category: "WorldLoadComponent")
6 |
7 | /// Loads chunks from the world model into the SceneKit node.
8 | class WorldLoadComponent: GKComponent {
9 | /// The 'reference-counts' of each chunk retainer (e.g. players).
10 | private var retainCounts: [ChunkPos: Int] = [:]
11 |
12 | /// The currently loaded chunks with their associated SceneKit nodes.
13 | private var loadedChunks: [ChunkPos: SCNNode] = [:]
14 |
15 | /// The chunks for which an unload has been requested.
16 | private var unloadRequestedChunks: Set = []
17 |
18 | /// Strips marked as dirty (e.g. because the user placed/removed blocks there).
19 | private var dirtyStrips: Set = []
20 |
21 | /// Performs occlusion checking before rendering. Makes chunk loading slower and rendering faster.
22 | private var checkOcclusions: Bool = true
23 |
24 | /// Throttles chunk loading to a fixed interval.
25 | private var throttler = Throttler(interval: 0.5)
26 |
27 | /// Debounces chunk unloading to a fixed interval.
28 | private var debouncer = Debouncer(interval: 2)
29 |
30 | private var world: World? {
31 | get { entity?.component(ofType: WorldComponent.self)?.world }
32 | set { entity?.component(ofType: WorldComponent.self)?.world = newValue! }
33 | }
34 |
35 | private var node: SCNNode? {
36 | entity?.component(ofType: SceneNodeComponent.self)?.node
37 | }
38 |
39 | /// Increments the retain count for the given chunk.
40 | func retainChunk(at pos: ChunkPos) {
41 | retainCounts[pos] = (retainCounts[pos] ?? 0) + 1
42 | }
43 |
44 | /// Decrements the retain count for the given chunk.
45 | func releaseChunk(at pos: ChunkPos) {
46 | let newCount = (retainCounts[pos] ?? 1) - 1
47 | retainCounts[pos] = newCount == 0 ? nil : newCount
48 | }
49 |
50 | /// Marks a strip as dirty. Also marks the adjacent strips as dirty since occlusions might have changed.
51 | func markDirty(at pos: BlockPos2) {
52 | dirtyStrips.insert(pos)
53 | for neighbor in pos.neighbors {
54 | dirtyStrips.insert(neighbor)
55 | }
56 | }
57 |
58 | override func update(deltaTime seconds: TimeInterval) {
59 | guard let node = node,
60 | let world = world else { return }
61 |
62 | throttler.submit(deltaTime: seconds) {
63 | // Perform a delta update of the chunks
64 | let requestedChunks = Set(retainCounts.keys)
65 | let currentChunks = Set(loadedChunks.keys)
66 | let chunksToLoad = requestedChunks.subtracting(currentChunks)
67 | let chunksToUnload = currentChunks.subtracting(requestedChunks)
68 | let stripsToReload = dirtyStrips.filter { !chunksToLoad.contains(ChunkPos(containing: $0)) }
69 |
70 | // Unload chunks by marking their chunks as requested for unloading
71 | // (we don't unload them directly by removing them from the loadedChunks and the scene since especially the latter seems to be an expensive operation).
72 | unloadRequestedChunks.subtract(chunksToLoad)
73 | unloadRequestedChunks.formUnion(chunksToUnload)
74 |
75 | if !chunksToLoad.isEmpty {
76 | log.info("Loading \(chunksToLoad.count) chunk(s)...")
77 | }
78 |
79 | // Load chunks by creating the corresponding scene nodes and attaching them to the world node
80 | for chunkPos in chunksToLoad {
81 | let chunkNode = loadChunk(at: chunkPos)
82 | node.addChildNode(chunkNode)
83 | loadedChunks[chunkPos] = chunkNode
84 | }
85 |
86 | // Reload dirty strips that aren't in the newly loaded chunks
87 | reload(strips: stripsToReload)
88 | } orElse: {
89 | // Reload all dirty strips immediately if there are any
90 | reload(strips: dirtyStrips)
91 | }
92 |
93 | let playersIdling = world.playerInfos.values.allSatisfy { $0.velocity == .zero }
94 | let unloadCount = unloadRequestedChunks.count
95 | let unloadingOverdue = unloadCount > 350
96 |
97 | debouncer.submit(deltaTime: seconds, defer: !playersIdling, force: unloadingOverdue) {
98 | if unloadCount > 0 {
99 | log.info("Unloading \(unloadCount) chunk(s)...")
100 | }
101 |
102 | // Unload chunks
103 | for chunkPos in unloadRequestedChunks {
104 | if let chunkNode = loadedChunks[chunkPos] {
105 | chunkNode.removeFromParentNode()
106 | loadedChunks[chunkPos] = nil
107 | }
108 | for pos in chunkPos {
109 | world.uncache(at: pos)
110 | }
111 | }
112 |
113 | unloadRequestedChunks = []
114 | }
115 |
116 | dirtyStrips = []
117 | }
118 |
119 | private func reload(strips: Set) {
120 | for pos in strips {
121 | let chunkPos = ChunkPos(containing: pos)
122 | if let chunkNode = loadedChunks[chunkPos] {
123 | // TODO: Investigate efficiency here?
124 | for blockNode in chunkNode.childNodes where BlockPos3(rounding: blockNode.position).asVec2 == pos {
125 | blockNode.removeFromParentNode()
126 | }
127 | loadStrip(at: pos, into: chunkNode)
128 | }
129 | }
130 | }
131 |
132 | private func loadChunk(at chunkPos: ChunkPos) -> SCNNode {
133 | let chunkNode = SCNNode()
134 |
135 | for pos in chunkPos {
136 | loadStrip(at: pos, into: chunkNode)
137 | }
138 |
139 | return chunkNode
140 | }
141 |
142 | private func loadStrip(at pos: BlockPos2, into chunkNode: SCNNode) {
143 | guard let world = world else { return }
144 |
145 | for (y, block) in world[pos] {
146 | let blockPos = pos.with(y: y)
147 | // Only add blocks that aren't fully occluded
148 | if !checkOcclusions || !world.isOccluded(at: blockPos) {
149 | let blockNode = makeBlockNode(for: block)
150 | blockNode.position = SCNVector3(blockPos)
151 | chunkNode.addChildNode(blockNode)
152 | }
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/Component/PlayerControlComponent.swift:
--------------------------------------------------------------------------------
1 | import GameplayKit
2 |
3 | private let angularValocityEpsilon: SceneFloat = 0.01
4 | private let piHalf = SceneFloat.pi / 2
5 | private let velocityId = "PlayerControlComponent"
6 |
7 | /// Lets the user control the associated player.
8 | class PlayerControlComponent: GKComponent {
9 | /// The current motion input.
10 | private var motionInput: MotionInput = []
11 | /// Achievements since the last SceneKit update. We batch these to avoid mutating the world from the main thread.
12 | private var newAchievements: Achievements = []
13 |
14 | private var baseSpeed: Double = 0.5
15 | private var flightFactor: Double = 2
16 | private var sprintFactor: Double = 1.8
17 | private var pitchSpeed: SceneFloat = 0.4
18 | private var yawSpeed: SceneFloat = 0.3
19 | private var jumpSpeed: Double = 1
20 | private var ascendSpeed: Double = 1
21 | private var maxCollisionIterations: Int = 5
22 | private var pitchRange: ClosedRange = -piHalf...piHalf
23 |
24 | private var blockThrottler = Throttler(interval: 0.15)
25 |
26 | private var speed: Double {
27 | baseSpeed
28 | * (motionInput.contains(.sprint) ? sprintFactor : 1)
29 | * ((playerInfo?.gameMode.permitsFlight ?? false) ? flightFactor : 1)
30 | }
31 |
32 | private var node: SCNNode? {
33 | entity?.component(ofType: SceneNodeComponent.self)?.node
34 | }
35 |
36 | private var worldAssocationComponent: WorldAssociationComponent? {
37 | entity?.component(ofType: WorldAssociationComponent.self)
38 | }
39 |
40 | private var worldNode: SCNNode? {
41 | worldAssocationComponent?.worldNode
42 | }
43 |
44 | private var world: World? {
45 | get { worldAssocationComponent?.world }
46 | set { worldAssocationComponent?.world = newValue! }
47 | }
48 |
49 | private var worldLoadComponent: WorldLoadComponent? {
50 | worldAssocationComponent?.worldLoadComponent
51 | }
52 |
53 | private var handLoadComponent: HandLoadComponent? {
54 | entity?.component(ofType: HandLoadComponent.self)
55 | }
56 |
57 | private var playerAssociationComponent: PlayerAssociationComponent? {
58 | entity?.component(ofType: PlayerAssociationComponent.self)
59 | }
60 |
61 | private var playerInfo: PlayerInfo? {
62 | get { playerAssociationComponent?.playerInfo }
63 | set { playerAssociationComponent?.playerInfo = newValue! }
64 | }
65 |
66 | private var lookAtBlockComponent: LookAtBlockComponent? {
67 | entity?.component(ofType: LookAtBlockComponent.self)
68 | }
69 |
70 | var requestedBaseVelocity: Vec3 = Vec3() {
71 | didSet {
72 | achieve(.moveAround)
73 | }
74 | }
75 |
76 | private var requestedVelocity: Vec3? {
77 | guard let node = node, let parent = node.parent else { return nil }
78 |
79 | var vector = SCNVector3(requestedBaseVelocity)
80 |
81 | if motionInput.contains(.forward) {
82 | vector.z -= 1
83 | }
84 | if motionInput.contains(.back) {
85 | vector.z += 1
86 | }
87 | if motionInput.contains(.left) {
88 | vector.x -= 1
89 | }
90 | if motionInput.contains(.right) {
91 | vector.x += 1
92 | }
93 |
94 | var rotated = node.convertVector(vector, to: parent)
95 | rotated.y = 0 // disable vertical movement
96 |
97 | if rotated.length > 0 {
98 | rotated.normalize()
99 | rotated *= SceneFloat(speed)
100 | }
101 |
102 | return Vec3(rotated)
103 | }
104 |
105 | /// A bit set that represents motion input, usually by the user.
106 | struct MotionInput: OptionSet {
107 | static let forward = MotionInput(rawValue: 1 << 0)
108 | static let back = MotionInput(rawValue: 1 << 1)
109 | static let left = MotionInput(rawValue: 1 << 2)
110 | static let right = MotionInput(rawValue: 1 << 3)
111 | static let jump = MotionInput(rawValue: 1 << 8)
112 | static let sprint = MotionInput(rawValue: 1 << 9)
113 | static let breakBlock = MotionInput(rawValue: 1 << 10)
114 | static let useBlock = MotionInput(rawValue: 1 << 11)
115 | static let sneak = MotionInput(rawValue: 1 << 12)
116 |
117 | let rawValue: UInt16
118 |
119 | init(rawValue: UInt16) {
120 | self.rawValue = rawValue
121 | }
122 | }
123 |
124 | override func update(deltaTime seconds: TimeInterval) {
125 | // Note that we don't use the if-var-and-assign idiom for playerInfo due to responsiveness issues (and inout bindings aren't in Swift yet)
126 | guard let requestedVelocity = requestedVelocity,
127 | playerInfo != nil else { return }
128 |
129 | // Fetch position and velocity
130 | let position = playerInfo!.position
131 | var velocity = playerInfo!.velocity
132 | let gameMode = playerInfo!.gameMode
133 |
134 | // Running into terrain pushes the player back, causing them to 'slide' along the block.
135 | // For more info, look up 'AABB sliding collision response'.
136 | let feetPos = position
137 | var finalVelocity = requestedVelocity
138 | var iterations = 0
139 |
140 | if gameMode.enablesGravityAndCollisions {
141 | while let hit = worldNode?.hitTestWithSegment(from: SCNVector3(feetPos), to: SCNVector3(feetPos + finalVelocity)).first, iterations < maxCollisionIterations {
142 | let normal = Vec3(hit.worldNormal)
143 | let repulsion = normal * abs(finalVelocity.dot(normal))
144 | finalVelocity += repulsion
145 | iterations += 1
146 | }
147 | }
148 |
149 | // Apply the movement
150 | velocity.x = finalVelocity.x
151 | velocity.z = finalVelocity.z
152 |
153 | if gameMode.permitsFlight {
154 | // Ascend/descend as needed
155 | velocity.y = 0
156 | if motionInput.contains(.jump) {
157 | velocity.y += ascendSpeed
158 | }
159 | if motionInput.contains(.sneak) {
160 | velocity.y -= ascendSpeed
161 | }
162 | } else {
163 | // Jump if needed/possible
164 | if motionInput.contains(.jump) && playerInfo!.isOnGround {
165 | velocity.y = jumpSpeed
166 | playerInfo!.leavesGround = true
167 | }
168 | }
169 |
170 | blockThrottler.submit(deltaTime: seconds) {
171 | // Break looked-at block if needed
172 | if motionInput.contains(.breakBlock) {
173 | breakBlock()
174 | }
175 |
176 | // Place on looked-at block if needed
177 | if motionInput.contains(.useBlock) {
178 | useBlock()
179 | }
180 | }
181 |
182 | playerInfo!.position = position
183 | playerInfo!.velocity = velocity
184 |
185 | flushAchievements()
186 | }
187 |
188 | /// Adds motion input.
189 | func add(motionInput delta: MotionInput) {
190 | motionInput.insert(delta)
191 |
192 | if !delta.isDisjoint(with: [.forward, .back, .left, .right]) {
193 | achieve(.moveAround)
194 | }
195 | if delta.contains(.sprint) {
196 | achieve(.sprint)
197 | }
198 | if delta.contains(.jump) {
199 | achieve(.jump)
200 | }
201 | if delta.contains(.breakBlock) {
202 | achieve(.breakBlock)
203 | }
204 | if delta.contains(.useBlock) {
205 | achieve(.useBlock)
206 | }
207 |
208 | if delta.contains(.useBlock) || delta.contains(.breakBlock) {
209 | blockThrottler.reset()
210 | }
211 | }
212 |
213 | /// Removes motion input.
214 | func remove(motionInput delta: MotionInput) {
215 | motionInput.remove(delta)
216 | }
217 |
218 | /// Rotates the node vertically by the given angle (in radians).
219 | func rotatePitch(by delta: SceneFloat) {
220 | guard let node = node, canRotatePitch(by: delta) else { return }
221 | achieve(.peekAround)
222 | node.eulerAngles.x += delta * pitchSpeed
223 | }
224 |
225 | /// Rotates the node horizontally by the given angle (in radians).
226 | func rotateYaw(by delta: SceneFloat) {
227 | achieve(.peekAround)
228 | node?.eulerAngles.y += delta * yawSpeed
229 | }
230 |
231 | private func canRotatePitch(by delta: SceneFloat) -> Bool {
232 | guard let node = node else { return true }
233 | return pitchRange.contains(node.eulerAngles.x + delta * pitchSpeed)
234 | }
235 |
236 | func moveHotbarSelection(by delta: Int) {
237 | achieve(.hotbar)
238 | playerInfo?.selectedHotbarSlot += delta
239 | }
240 |
241 | func select(hotbarSlot: Int) {
242 | achieve(.hotbar)
243 | playerInfo?.selectedHotbarSlot = hotbarSlot
244 | }
245 |
246 | /// Toggles the debug overlay for the player.
247 | func toggleDebugHUD() {
248 | playerInfo?.hasDebugHUDEnabled = !(playerInfo?.hasDebugHUDEnabled ?? true)
249 | }
250 |
251 | func jump() {
252 | guard playerInfo?.isOnGround ?? false else { return }
253 | achieve(.jump)
254 | playerInfo?.velocity.y = jumpSpeed
255 | playerInfo?.leavesGround = true
256 | }
257 |
258 | func breakBlock() {
259 | guard let lookedAtBlockPos = lookAtBlockComponent?.blockPos else { return }
260 | world?.breakBlock(at: lookedAtBlockPos)
261 | worldLoadComponent?.markDirty(at: lookedAtBlockPos.asVec2)
262 | handLoadComponent?.swing()
263 | achieve(.breakBlock)
264 | }
265 |
266 | func useBlock() {
267 | guard let placePos = lookAtBlockComponent?.blockPlacePos,
268 | let feetPos = playerInfo?.position,
269 | case let .block(blockType)? = playerInfo?.selectedHotbarStack?.item.type,
270 | placePos != BlockPos3(rounding: feetPos) else { return }
271 | // TODO: Decrement item stack
272 | world?.place(block: Block(type: blockType), at: placePos)
273 | worldLoadComponent?.markDirty(at: placePos.asVec2)
274 | handLoadComponent?.swing()
275 | achieve(.useBlock)
276 | }
277 |
278 | private func achieve(_ delta: Achievements) {
279 | newAchievements.insert(delta)
280 | }
281 |
282 | private func flushAchievements() {
283 | playerInfo?.achieve(newAchievements)
284 | newAchievements = []
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/MiniBlocks.swiftpm/Sources/View/MiniBlocksViewController.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreGraphics
3 | import SceneKit
4 | import SpriteKit
5 | import GameController
6 | import GameplayKit
7 | import OSLog
8 |
9 | private let log = Logger(subsystem: "MiniBlocks", category: "MiniBlocksViewController")
10 |
11 | /// The game's primary view controller, responsible for
12 | /// presenting the scene and handling input. Depending on
13 | /// the platform, input is either handled using AppKit (macOS)
14 | /// or UIKit/GameController (iOS/iPadOS).
15 | public final class MiniBlocksViewController: ViewController, SCNSceneRendererDelegate, GestureRecognizerDelegate {
16 | private let playerName: String
17 | private let gameMode: GameMode
18 | private let worldGenerator: WorldGeneratorType
19 | private let renderDistance: Int
20 | private let ambientOcclusionEnabled: Bool
21 | private let debugStatsShown: Bool
22 | private let achievementsShown: Bool
23 | private let handShown: Bool
24 | private var previousUpdateTime: TimeInterval = 0
25 |
26 | // MARK: View properties
27 |
28 | private var sceneView: MiniBlocksSceneView!
29 | private let sceneFrame: CGRect?
30 | private var inputSensivity: SceneFloat = 1
31 |
32 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
33 | @Box private var usesMouseKeyboardControls = true
34 | private var receivedFirstMouseEvent: Bool = false
35 | private var mouseCaptured: Bool = false {
36 | willSet {
37 | if newValue != mouseCaptured {
38 | // Update actual capturing
39 | if newValue {
40 | receivedFirstMouseEvent = false
41 | warpMouseCursorToCenter()
42 | CGDisplayHideCursor(kCGNullDirectDisplay)
43 | } else {
44 | CGDisplayShowCursor(kCGNullDirectDisplay)
45 | }
46 |
47 | // Notify component system
48 | for case let component as MouseCaptureVisibilityComponent in mouseCaptureVisibilityComponentSystem.components {
49 | component.update(mouseCaptured: newValue)
50 | }
51 | }
52 | }
53 | }
54 | #endif
55 |
56 | #if canImport(UIKit)
57 | @Box private var usesMouseKeyboardControls = false
58 | private var panDragStart: CGPoint?
59 | private var panDraggedComponent: TouchInteractable?
60 | private var panDragRecognizer: UIPanGestureRecognizer!
61 | private var panCameraRecognizer: UIPanGestureRecognizer!
62 | private var tapRecognizer: UITapGestureRecognizer!
63 | private var pressRecognizer: UILongPressGestureRecognizer!
64 | public override var prefersHomeIndicatorAutoHidden: Bool { true }
65 | public override var prefersPointerLocked: Bool { true }
66 | #endif
67 |
68 | // MARK: SpriteKit/SceneKit properties
69 |
70 | private var overlayScene: SKScene!
71 | private var scene: SCNScene!
72 |
73 | // MARK: GameplayKit properties
74 |
75 | private let playerControlComponentSystem = GKComponentSystem(componentClass: PlayerControlComponent.self)
76 | private let playerPositioningComponentSystem = GKComponentSystem(componentClass: PlayerPositioningComponent.self)
77 | private let lookAtBlockComponentSystem = GKComponentSystem(componentClass: LookAtBlockComponent.self)
78 | private let playerGravityComponentSystem = GKComponentSystem(componentClass: PlayerGravityComponent.self)
79 | private let worldLoadComponentSystem = GKComponentSystem(componentClass: WorldLoadComponent.self)
80 | private let worldRetainComponentSystem = GKComponentSystem(componentClass: WorldRetainComponent.self)
81 | private let handLoadComponentSystem = GKComponentSystem(componentClass: HandLoadComponent.self)
82 | private let hotbarHUDLoadComponentSystem = GKComponentSystem(componentClass: HotbarHUDLoadComponent.self)
83 | private let debugHUDLoadComponentSystem = GKComponentSystem(componentClass: DebugHUDLoadComponent.self)
84 | private let achievementHUDLoadComponentSystem = GKComponentSystem(componentClass: AchievementHUDLoadComponent.self)
85 | private let mouseCaptureVisibilityComponentSystem = GKComponentSystem(componentClass: MouseCaptureVisibilityComponent.self)
86 | private var playerEntity: GKEntity!
87 | private var controlPadHUDEntity: GKEntity?
88 | private var entities: [GKEntity] = []
89 |
90 | public init(
91 | sceneFrame: CGRect? = nil,
92 | playerName: String = "Player",
93 | worldGenerator: WorldGeneratorType = .nature(seed: "default"),
94 | gameMode: GameMode = .survival,
95 | renderDistance: Int = 8,
96 | ambientOcclusionEnabled: Bool = false,
97 | debugStatsShown: Bool = false,
98 | achievementsShown: Bool = true,
99 | handShown: Bool = true
100 | ) {
101 | self.sceneFrame = sceneFrame
102 | self.playerName = playerName
103 | self.worldGenerator = worldGenerator
104 | self.gameMode = gameMode
105 | self.renderDistance = renderDistance
106 | self.ambientOcclusionEnabled = ambientOcclusionEnabled
107 | self.debugStatsShown = debugStatsShown
108 | self.achievementsShown = achievementsShown
109 | self.handShown = handShown
110 |
111 | super.init(nibName: nil, bundle: nil)
112 | }
113 |
114 | public required init?(coder: NSCoder) {
115 | nil
116 | }
117 |
118 | /// Creates the initial scene.
119 | public override func loadView() {
120 | // Create main scene
121 | scene = SCNScene(named: "MiniBlocksScene.scn")!
122 |
123 | // Create overlay scene
124 | overlayScene = sceneFrame.map { SKScene(size: $0.size) } ?? SKScene()
125 | overlayScene.scaleMode = .aspectFill
126 | overlayScene.isUserInteractionEnabled = false
127 |
128 | // Add light
129 | add(entity: makeSunEntity())
130 | add(entity: makeAmbientLightEntity())
131 |
132 | // Add the world
133 | let world = World(generator: worldGenerator)
134 | let worldEntity = makeWorldEntity(world: world)
135 | add(entity: worldEntity)
136 |
137 | // Add player
138 | let playerSpawnPos2 = BlockPos2.zero
139 | let playerSpawnPos3 = playerSpawnPos2.with(y: world.height(at: playerSpawnPos2) ?? 10)
140 | playerEntity = makePlayerEntity(
141 | name: playerName,
142 | position: Vec3(playerSpawnPos3),
143 | gameMode: gameMode,
144 | worldEntity: worldEntity,
145 | retainRadius: renderDistance,
146 | ambientOcclusionEnabled: ambientOcclusionEnabled,
147 | handShown: handShown
148 | )
149 | add(entity: playerEntity)
150 |
151 | // Add overlay HUD
152 | add(entity: makeCrosshairHUDEntity(in: overlayScene.frame))
153 | add(entity: makeHotbarHUDEntity(in: overlayScene.frame, playerEntity: playerEntity))
154 | add(entity: makeDebugHUDEntity(in: overlayScene.frame, playerEntity: playerEntity))
155 |
156 | if achievementsShown {
157 | add(entity: makeAchievementHUDEntity(in: overlayScene.frame, playerEntity: playerEntity, usesMouseKeyboardControls: _usesMouseKeyboardControls))
158 | }
159 |
160 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
161 | add(entity: makePauseHUDEntity(in: overlayScene.frame))
162 | #endif
163 |
164 | // Set up SCNView
165 | sceneView = sceneFrame.map { MiniBlocksSceneView(frame: $0) } ?? MiniBlocksSceneView()
166 | sceneView.scene = scene
167 | sceneView.delegate = self
168 | sceneView.allowsCameraControl = false
169 | sceneView.showsStatistics = debugStatsShown
170 | sceneView.backgroundColor = Color.black
171 | sceneView.overlaySKScene = overlayScene
172 | sceneView.antialiasingMode = .none
173 | sceneView.isJitteringEnabled = false
174 |
175 | // Keep scene active, otherwise it will stop sending renderer(_:updateAtTime:)s when nothing changes. See also https://stackoverflow.com/questions/39336509/how-do-you-set-up-a-game-loop-for-scenekit
176 | sceneView.isPlaying = true
177 |
178 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
179 | // Set up mouse/keyboard handling when using AppKit (on macOS)
180 | sceneView.keyEventsDelegate = self
181 |
182 | if let sceneFrame = sceneFrame {
183 | sceneView.addTrackingArea(NSTrackingArea(
184 | rect: sceneFrame,
185 | options: [.activeAlways, .mouseMoved, .inVisibleRect],
186 | owner: self,
187 | userInfo: nil
188 | ))
189 | }
190 | #endif
191 |
192 | #if canImport(UIKit)
193 | // Set up mouse/keyboard handling via the GameController framework (on iOS)
194 | // TODO: Use GameController-based input on macOS too (replacing AppKit)
195 | let center = NotificationCenter.default
196 | center.addObserver(forName: .GCMouseDidConnect, object: nil, queue: .main) {
197 | if let mouse = $0.object as? GCMouse {
198 | self.usesMouseKeyboardControls = true
199 | self.deregisterUITouchControls()
200 | self.registerHandlers(for: mouse)
201 | }
202 | }
203 | center.addObserver(forName: .GCMouseDidBecomeCurrent, object: nil, queue: .main) { _ in
204 | self.usesMouseKeyboardControls = true
205 | self.deregisterUITouchControls()
206 | }
207 | center.addObserver(forName: .GCMouseDidDisconnect, object: nil, queue: .main) {
208 | if let mouse = $0.object as? GCMouse {
209 | self.usesMouseKeyboardControls = false
210 | self.registerUITouchControls()
211 | self.deregisterHandlers(from: mouse)
212 | }
213 | }
214 | center.addObserver(forName: .GCKeyboardDidConnect, object: nil, queue: .main) {
215 | if let keyboard = $0.object as? GCKeyboard {
216 | self.registerHandlers(for: keyboard)
217 | }
218 | }
219 |
220 | // Set up touch/gesture controls if not using a mouse
221 | let panDragRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanDrag(_:)))
222 | panDragRecognizer.delegate = self
223 |
224 | let panCameraRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePanCamera(_:)))
225 | panCameraRecognizer.delegate = self
226 |
227 | let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
228 | tapRecognizer.delegate = self
229 |
230 | let pressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
231 | pressRecognizer.minimumPressDuration = 0.5
232 | pressRecognizer.delegate = self
233 |
234 | self.panDragRecognizer = panDragRecognizer
235 | self.panCameraRecognizer = panCameraRecognizer
236 | self.tapRecognizer = tapRecognizer
237 | self.pressRecognizer = pressRecognizer
238 |
239 | if GCMouse.current == nil {
240 | registerUITouchControls()
241 | }
242 | #endif
243 |
244 | view = sceneView
245 | log.info("Loaded view")
246 | }
247 |
248 | private func add(entity: GKEntity) {
249 | entities.append(entity)
250 |
251 | // Add attached node to the scene, if entity has the corresponding component
252 | if let node = entity.component(ofType: SceneNodeComponent.self)?.node {
253 | scene.rootNode.addChildNode(node)
254 | }
255 |
256 | // Add attached sprite node to the overlay, if present
257 | if let node = entity.component(ofType: SpriteNodeComponent.self)?.node {
258 | DispatchQueue.main.async { [self] in
259 | overlayScene.addChild(node)
260 | }
261 | }
262 |
263 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
264 | // Provide initial update to mouse capture visibility component
265 | if let component = entity.component(ofType: MouseCaptureVisibilityComponent.self) {
266 | component.update(mouseCaptured: mouseCaptured)
267 | }
268 | #endif
269 |
270 | // Add components to their corresponding systems
271 | playerControlComponentSystem.addComponent(foundIn: entity)
272 | playerPositioningComponentSystem.addComponent(foundIn: entity)
273 | playerGravityComponentSystem.addComponent(foundIn: entity)
274 | lookAtBlockComponentSystem.addComponent(foundIn: entity)
275 | worldLoadComponentSystem.addComponent(foundIn: entity)
276 | worldRetainComponentSystem.addComponent(foundIn: entity)
277 | handLoadComponentSystem.addComponent(foundIn: entity)
278 | hotbarHUDLoadComponentSystem.addComponent(foundIn: entity)
279 | debugHUDLoadComponentSystem.addComponent(foundIn: entity)
280 | achievementHUDLoadComponentSystem.addComponent(foundIn: entity)
281 | mouseCaptureVisibilityComponentSystem.addComponent(foundIn: entity)
282 | }
283 |
284 | private func remove(entity: GKEntity) {
285 | entities.removeAll { $0 === entity }
286 |
287 | // Remove attached scene node if needed
288 | if let node = entity.component(ofType: SceneNodeComponent.self)?.node {
289 | node.removeFromParentNode()
290 | }
291 |
292 | // Remove attached sprite note if needed
293 | if let node = entity.component(ofType: SpriteNodeComponent.self)?.node {
294 | DispatchQueue.main.async {
295 | node.removeFromParent()
296 | }
297 | }
298 |
299 | // Remove components from their corresponding systems
300 | playerControlComponentSystem.removeComponent(foundIn: entity)
301 | playerPositioningComponentSystem.removeComponent(foundIn: entity)
302 | playerGravityComponentSystem.removeComponent(foundIn: entity)
303 | lookAtBlockComponentSystem.removeComponent(foundIn: entity)
304 | worldLoadComponentSystem.removeComponent(foundIn: entity)
305 | worldRetainComponentSystem.removeComponent(foundIn: entity)
306 | handLoadComponentSystem.removeComponent(foundIn: entity)
307 | hotbarHUDLoadComponentSystem.removeComponent(foundIn: entity)
308 | debugHUDLoadComponentSystem.removeComponent(foundIn: entity)
309 | achievementHUDLoadComponentSystem.removeComponent(foundIn: entity)
310 | mouseCaptureVisibilityComponentSystem.removeComponent(foundIn: entity)
311 | }
312 |
313 | public func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
314 | let deltaTime = time - previousUpdateTime
315 |
316 | // Perform updates to the components through their corresponding systems
317 | playerControlComponentSystem.update(deltaTime: deltaTime)
318 | playerPositioningComponentSystem.update(deltaTime: deltaTime)
319 | playerGravityComponentSystem.update(deltaTime: deltaTime)
320 | lookAtBlockComponentSystem.update(deltaTime: deltaTime)
321 | worldLoadComponentSystem.update(deltaTime: deltaTime)
322 | worldRetainComponentSystem.update(deltaTime: deltaTime)
323 | handLoadComponentSystem.update(deltaTime: deltaTime)
324 | hotbarHUDLoadComponentSystem.update(deltaTime: deltaTime)
325 | debugHUDLoadComponentSystem.update(deltaTime: deltaTime)
326 | achievementHUDLoadComponentSystem.update(deltaTime: deltaTime)
327 | mouseCaptureVisibilityComponentSystem.update(deltaTime: deltaTime)
328 |
329 | previousUpdateTime = time
330 | }
331 |
332 | private func controlPlayer(with action: (PlayerControlComponent) -> Void) {
333 | for case let component as PlayerControlComponent in playerControlComponentSystem.components {
334 | action(component)
335 | }
336 | }
337 |
338 | // MARK: GameController-based mouse/keyboard controls
339 |
340 | #if canImport(UIKit)
341 |
342 | private func registerGCMouseKeyboardControls() {
343 | if let mouse = GCMouse.current {
344 | registerHandlers(for: mouse)
345 | }
346 | if let keyboard = GCKeyboard.coalesced {
347 | registerHandlers(for: keyboard)
348 | }
349 | }
350 |
351 | private func registerHandlers(for mouse: GCMouse) {
352 | guard let input = mouse.mouseInput else { return }
353 | input.mouseMovedHandler = { (_, dx, dy) in
354 | self.controlPlayer { component in
355 | component.rotateYaw(by: -(SceneFloat(dx) * self.inputSensivity) / 100)
356 | component.rotatePitch(by: (SceneFloat(dy) * self.inputSensivity) / 100)
357 | }
358 | }
359 | input.scroll.valueChangedHandler = { (_, dx, dy) in
360 | let slotDelta = Int(dx + dy)
361 |
362 | // Move the selected slot
363 | self.controlPlayer { component in
364 | component.moveHotbarSelection(by: slotDelta)
365 | }
366 | }
367 | input.leftButton.valueChangedHandler = { (_, _, pressed) in
368 | self.controlPlayer { component in
369 | if pressed {
370 | component.add(motionInput: .breakBlock)
371 | } else {
372 | component.remove(motionInput: .breakBlock)
373 | }
374 | }
375 | }
376 | input.rightButton?.valueChangedHandler = { (_, _, pressed) in
377 | self.controlPlayer { component in
378 | if pressed {
379 | component.add(motionInput: .useBlock)
380 | } else {
381 | component.remove(motionInput: .useBlock)
382 | }
383 | }
384 | }
385 | }
386 |
387 | private func deregisterHandlers(from mouse: GCMouse) {
388 | guard let input = mouse.mouseInput else { return }
389 | input.mouseMovedHandler = nil
390 | input.scroll.valueChangedHandler = nil
391 | input.leftButton.valueChangedHandler = nil
392 | input.rightButton?.valueChangedHandler = nil
393 | }
394 |
395 | private func registerHandlers(for keyboard: GCKeyboard) {
396 | guard let input = keyboard.keyboardInput else { return }
397 | input.keyChangedHandler = { (_, _, keyCode, down) in
398 | if down {
399 | self.keyDown(with: keyCode)
400 | } else {
401 | self.keyUp(with: keyCode)
402 | }
403 | }
404 | }
405 |
406 | private func keyDown(with keyCode: GCKeyCode) {
407 | if keyCode == .F3 {
408 | // Toggle debug information shown as an overlay (e.g. the current position)
409 | controlPlayer { component in
410 | component.toggleDebugHUD()
411 | }
412 | } else if let n = keyCode.numericValue {
413 | if (1...InventoryConstants.hotbarSlotCount).contains(n) {
414 | // Select hotbar slot
415 | controlPlayer { component in
416 | component.select(hotbarSlot: n - 1)
417 | }
418 | }
419 | } else {
420 | let motion = motionInput(for: keyCode)
421 | // Pressed key could be mapped to motion input, add it to the corresponding components
422 | controlPlayer { component in
423 | component.add(motionInput: motion)
424 | }
425 | }
426 | }
427 |
428 | private func keyUp(with keyCode: GCKeyCode) {
429 | let motion = motionInput(for: keyCode)
430 | // Pressed key could be mapped motion input, remove it from the corresponding components
431 | controlPlayer { component in
432 | component.remove(motionInput: motion)
433 | }
434 | }
435 |
436 | private func motionInput(for keyCode: GCKeyCode) -> PlayerControlComponent.MotionInput {
437 | switch keyCode {
438 | case .keyW: return .forward
439 | case .keyS: return .back
440 | case .keyA: return .left
441 | case .keyD: return .right
442 | case .spacebar: return .jump
443 | case .leftShift, .rightShift: return .sprint
444 | case .leftControl, .rightControl: return .sneak
445 | default: return []
446 | }
447 | }
448 |
449 | #endif
450 |
451 | // MARK: AppKit-based mouse/keyboard controls
452 |
453 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
454 |
455 | public override func keyDown(with event: NSEvent) {
456 | guard !event.isARepeat else { return }
457 | let keyCode = KeyCode(rawValue: event.keyCode)
458 |
459 | if keyCode == .escape {
460 | // Uncapture cursor when user presses escape
461 | mouseCaptured = false
462 | } else if keyCode == .f3 {
463 | // Toggle debug information shown as an overlay (e.g. the current position)
464 | controlPlayer { component in
465 | component.toggleDebugHUD()
466 | }
467 | } else if let n = keyCode.numericValue {
468 | if (1...InventoryConstants.hotbarSlotCount).contains(n) {
469 | // Select hotbar slot
470 | controlPlayer { component in
471 | component.select(hotbarSlot: n - 1)
472 | }
473 | }
474 | } else {
475 | let motion = motionInput(for: keyCode)
476 | // Pressed key could be mapped to motion input, add it to the corresponding components
477 | controlPlayer { component in
478 | component.add(motionInput: motion)
479 | }
480 | }
481 | }
482 |
483 | public override func keyUp(with event: NSEvent) {
484 | let motion = motionInput(for: KeyCode(rawValue: event.keyCode))
485 | // Pressed key could be mapped to motion input, remove it from the corresponding components
486 | controlPlayer { component in
487 | component.remove(motionInput: motion)
488 | }
489 | }
490 |
491 | public override func flagsChanged(with event: NSEvent) {
492 | let flags = event.modifierFlags
493 |
494 | // Sprint on shift
495 | controlPlayer { component in
496 | if flags.contains(.shift) {
497 | component.add(motionInput: .sprint)
498 | } else {
499 | component.remove(motionInput: .sprint)
500 | }
501 |
502 | if flags.contains(.control) {
503 | component.add(motionInput: .sneak)
504 | } else {
505 | component.remove(motionInput: .sneak)
506 | }
507 | }
508 | }
509 |
510 | public override func mouseDown(with event: NSEvent) {
511 | if mouseCaptured {
512 | // Break blocks on left-click if captured
513 | controlPlayer { component in
514 | component.add(motionInput: .breakBlock)
515 | }
516 | } else {
517 | // Capture mouse otherwise
518 | mouseCaptured = true
519 | }
520 | }
521 |
522 | public override func rightMouseDown(with event: NSEvent) {
523 | if mouseCaptured {
524 | // Use blocks on right-click if captured
525 | controlPlayer { component in
526 | component.add(motionInput: .useBlock)
527 | }
528 | }
529 | }
530 |
531 | public override func mouseUp(with event: NSEvent) {
532 | if mouseCaptured {
533 | // Stop breaking blocks
534 | controlPlayer { component in
535 | component.remove(motionInput: .breakBlock)
536 | }
537 | }
538 | }
539 |
540 | public override func rightMouseUp(with event: NSEvent) {
541 | if mouseCaptured {
542 | // Stop using blocks
543 | controlPlayer { component in
544 | component.remove(motionInput: .useBlock)
545 | }
546 | }
547 | }
548 |
549 | public override func mouseMoved(with event: NSEvent) {
550 | if mouseCaptured {
551 | // Skip first event since this one may have large deltas
552 | guard receivedFirstMouseEvent else {
553 | receivedFirstMouseEvent = true
554 | return
555 | }
556 |
557 | // Rotate camera
558 | controlPlayer { component in
559 | component.rotateYaw(by: -(event.deltaX * inputSensivity) / 50)
560 | component.rotatePitch(by: -(event.deltaY * inputSensivity) / 50)
561 | }
562 |
563 | // Keep mouse at center of window
564 | warpMouseCursorToCenter()
565 | }
566 | }
567 |
568 | public override func scrollWheel(with event: NSEvent) {
569 | if mouseCaptured {
570 | let slotDelta = Int(event.scrollingDeltaX + event.scrollingDeltaY)
571 |
572 | // Move the selected slot
573 | controlPlayer { component in
574 | component.moveHotbarSelection(by: slotDelta)
575 | }
576 | }
577 | }
578 |
579 | public override func mouseDragged(with event: NSEvent) {
580 | mouseMoved(with: event)
581 | }
582 |
583 | public override func rightMouseDragged(with event: NSEvent) {
584 | mouseMoved(with: event)
585 | }
586 |
587 | private func warpMouseCursorToCenter() {
588 | if let window = view.window, let screen = window.screen {
589 | let frameInWindow = view.convert(view.bounds, to: nil)
590 | let frameOnScreen = window.convertToScreen(frameInWindow)
591 | let midX = frameOnScreen.midX
592 | // AppKit and Quartz use different coordinate systems so we need to convert here
593 | let midY = screen.frame.size.height - frameOnScreen.midY
594 | CGWarpMouseCursorPosition(CGPoint(x: midX, y: midY))
595 | }
596 | }
597 |
598 | private func motionInput(for keyCode: KeyCode) -> PlayerControlComponent.MotionInput {
599 | switch keyCode {
600 | case .w: return .forward
601 | case .s: return .back
602 | case .a: return .left
603 | case .d: return .right
604 | case .space: return .jump
605 | default: return []
606 | }
607 | }
608 |
609 | #endif
610 |
611 | // MARK: Touch controls
612 |
613 | #if canImport(UIKit)
614 |
615 | private func registerUITouchControls() {
616 | let controlPadHUDEntity = makeControlPadHUDEntity(in: overlayScene.frame, playerEntity: playerEntity)
617 | add(entity: controlPadHUDEntity)
618 | self.controlPadHUDEntity = controlPadHUDEntity
619 |
620 | sceneView.addGestureRecognizer(panDragRecognizer)
621 | sceneView.addGestureRecognizer(panCameraRecognizer)
622 | sceneView.addGestureRecognizer(tapRecognizer)
623 | sceneView.addGestureRecognizer(pressRecognizer)
624 | }
625 |
626 | private func deregisterUITouchControls() {
627 | if let controlPadHUDEntity = controlPadHUDEntity {
628 | remove(entity: controlPadHUDEntity)
629 | self.controlPadHUDEntity = nil
630 | }
631 |
632 | sceneView.removeGestureRecognizer(panDragRecognizer)
633 | sceneView.removeGestureRecognizer(panCameraRecognizer)
634 | sceneView.removeGestureRecognizer(tapRecognizer)
635 | sceneView.removeGestureRecognizer(pressRecognizer)
636 | }
637 |
638 | public func gestureRecognizer(_ recognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
639 | // Support multi-touch
640 | true
641 | }
642 |
643 | private func findDraggedComponent(for location: CGPoint) -> TouchInteractable? {
644 | let point = overlayScene.convertPoint(fromView: location)
645 | for entity in entities {
646 | for case let component as TouchInteractable in entity.components {
647 | if component.shouldReceiveDrag(at: point) {
648 | return component
649 | }
650 | }
651 | }
652 | return nil
653 | }
654 |
655 | public func gestureRecognizer(_ recognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
656 | guard [panCameraRecognizer, panDragRecognizer].contains(recognizer) else { return true }
657 | guard recognizer.numberOfTouches < 1 else { return false }
658 | let component = findDraggedComponent(for: touch.location(in: sceneView))
659 | return (component != nil) == (recognizer == panDragRecognizer)
660 | }
661 |
662 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
663 | return gestureRecognizer == tapRecognizer
664 | && otherGestureRecognizer == pressRecognizer
665 | }
666 |
667 | @objc
668 | private func handlePanDrag(_ recognizer: UIPanGestureRecognizer) {
669 | let location = recognizer.location(in: view)
670 | let delta = recognizer.velocity(in: view)
671 | let point = overlayScene.convertPoint(fromView: location)
672 | let start = overlayScene.convertPoint(fromView: panDragStart ?? location)
673 |
674 | switch recognizer.state {
675 | case .began:
676 | if let component = findDraggedComponent(for: location) {
677 | panDragStart = location
678 | panDraggedComponent = component
679 | component.onDragStart(at: point)
680 | } else {
681 | recognizer.state = .failed
682 | }
683 | case .changed:
684 | if let component = panDraggedComponent {
685 | // Forward drag to dragged component
686 | component.onDragMove(by: CGVector(dx: delta.x, dy: -delta.y), start: start, current: point)
687 | }
688 | case .ended:
689 | panDragStart = nil
690 | panDraggedComponent?.onDragEnd()
691 | panDraggedComponent = nil
692 | default:
693 | break
694 | }
695 | }
696 |
697 | @objc
698 | private func handlePanCamera(_ recognizer: UIPanGestureRecognizer) {
699 | let delta = recognizer.velocity(in: view)
700 | // Rotate camera
701 | controlPlayer { component in
702 | component.rotateYaw(by: (-SceneFloat(delta.x) * inputSensivity) / 800)
703 | component.rotatePitch(by: (-SceneFloat(delta.y) * inputSensivity) / 800)
704 | }
705 | }
706 |
707 | @objc
708 | private func handleTap(_ recognizer: UITapGestureRecognizer) {
709 | // Forward tap to TouchInteractable components
710 | let point = overlayScene.convertPoint(fromView: recognizer.location(in: sceneView))
711 | for entity in entities {
712 | for case let component as TouchInteractable in entity.components {
713 | if component.onTap(at: point) {
714 | return
715 | }
716 | }
717 | }
718 |
719 | // Respond to tap by using/placing a block
720 | controlPlayer { component in
721 | component.useBlock()
722 | }
723 | }
724 |
725 | @objc
726 | private func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
727 | controlPlayer { component in
728 | switch recognizer.state {
729 | case .began:
730 | component.add(motionInput: .breakBlock)
731 | case .ended:
732 | component.remove(motionInput: .breakBlock)
733 | default:
734 | break
735 | }
736 | }
737 | }
738 |
739 | #endif
740 | }
741 |
742 |
--------------------------------------------------------------------------------