├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── MiniBlocks.swiftpm ├── App │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 120.png │ │ │ ├── 152.png │ │ │ ├── 167.png │ │ │ ├── 180.png │ │ │ ├── 20.png │ │ │ ├── 29.png │ │ │ ├── 40.png │ │ │ ├── 58.png │ │ │ ├── 60.png │ │ │ ├── 80.png │ │ │ ├── 87.png │ │ │ └── Contents.json │ │ └── Contents.json │ ├── MiniBlocksApp.swift │ └── MiniBlocksView.swift ├── Package.swift ├── Resources │ ├── MiniBlocksScene.scn │ ├── TextureBedrock.png │ ├── TextureGrass.png │ ├── TextureLeaves.png │ ├── TextureSand.png │ ├── TextureStone.png │ ├── TextureWater.png │ └── TextureWood.png └── Sources │ ├── Component │ ├── AchievementHUDEntity.swift │ ├── AchievementHUDLoadComponent.swift │ ├── ControlPadHUDControlComponent.swift │ ├── DebugHUDLoadComponent.swift │ ├── HandLoadComponent.swift │ ├── HandNodeComponent.swift │ ├── HeightAboveGroundComponent.swift │ ├── HotbarHUDControlComponent.swift │ ├── HotbarHUDLoadComponent.swift │ ├── LookAtBlockComponent.swift │ ├── MouseCaptureVisibilityComponent.swift │ ├── NameComponent.swift │ ├── PlayerAssocationComponent.swift │ ├── PlayerControlComponent.swift │ ├── PlayerGravityComponent.swift │ ├── PlayerPositioningComponent.swift │ ├── SceneNodeComponent.swift │ ├── SpriteNodeComponent.swift │ ├── TouchInteractable.swift │ ├── WorldAssociationComponent.swift │ ├── WorldComponent.swift │ ├── WorldLoadComponent.swift │ └── WorldRetainComponent.swift │ ├── Entity │ ├── AmbientLightEntity.swift │ ├── ControlPadHUDEntity.swift │ ├── CrosshairHUDEntity.swift │ ├── DebugHUDEntity.swift │ ├── HotbarHUDEntity.swift │ ├── PauseHUDEntity.swift │ ├── PlayerEntity.swift │ ├── SunEntity.swift │ └── WorldEntity.swift │ ├── Model │ ├── Achievements.swift │ ├── Block.swift │ ├── BlockType.swift │ ├── EmptyWorldGenerator.swift │ ├── FlatWorldGenerator.swift │ ├── GameMode.swift │ ├── Inventory.swift │ ├── InventoryConstants.swift │ ├── Item.swift │ ├── ItemStack.swift │ ├── ItemType.swift │ ├── NatureWorldGenerator.swift │ ├── OceanWorldGenerator.swift │ ├── PlayerInfo.swift │ ├── Strip.swift │ ├── WavyHillsWorldGenerator.swift │ ├── World.swift │ ├── WorldGenerator.swift │ └── WorldGeneratorType.swift │ ├── Node │ ├── AchievementHUDNode.swift │ ├── BlockNode.swift │ ├── ControlPadHUDNode.swift │ ├── CrosshairHUDNode.swift │ ├── HotbarHUDSlotNode.swift │ ├── ItemNode.swift │ ├── NodeConstants.swift │ └── PauseHUDNode.swift │ ├── Utils │ ├── BlockPos2.swift │ ├── BlockPos3.swift │ ├── Box.swift │ ├── ChunkConstants.swift │ ├── ChunkPos.swift │ ├── ChunkRegion.swift │ ├── CompatibilityLayer.swift │ ├── CoreGraphicsUtils.swift │ ├── CyclicDeque.swift │ ├── Debouncer.swift │ ├── Deque.swift │ ├── FIFOCache.swift │ ├── GameControllerUtils.swift │ ├── GridIterator2.swift │ ├── MathUtils.swift │ ├── SceneKitUtils.swift │ ├── Throttler.swift │ ├── Vec2.swift │ ├── Vec2Convertible.swift │ ├── Vec2Protocol.swift │ ├── Vec3.swift │ ├── Vec3Convertible.swift │ ├── Vec3Protocol.swift │ └── Wraparound.swift │ └── View │ ├── KeyCode.swift │ ├── MiniBlocksSceneView.swift │ └── MiniBlocksViewController.swift ├── MiniBlocks ├── MiniBlocks.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── MiniBlocks.xcscheme ├── MiniBlocks │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ ├── 128.png │ │ │ ├── 16.png │ │ │ ├── 256.png │ │ │ ├── 32.png │ │ │ ├── 512.png │ │ │ ├── 64.png │ │ │ └── Contents.json │ │ └── Contents.json │ ├── MiniBlocks.entitlements │ └── main.swift ├── MiniBlocksTests │ ├── Model │ │ └── AchievementsTests.swift │ └── Utils │ │ ├── ChunkPosTests.swift │ │ ├── CyclicDequeTests.swift │ │ ├── DebouncerTests.swift │ │ ├── GridIterator2Tests.swift │ │ ├── MathUtilsTests.swift │ │ └── ThrottlerTests.swift └── MiniBlocksToGo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 120.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ └── Contents.json │ ├── Base.lproj │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── SceneDelegate.swift ├── README.md └── showcase.png /.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 | -------------------------------------------------------------------------------- /.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/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/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/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/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/App/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /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.swiftpm/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/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/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/Resources/MiniBlocksScene.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/MiniBlocksScene.scn -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Resources/TextureBedrock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/TextureBedrock.png -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Resources/TextureGrass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/TextureGrass.png -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Resources/TextureLeaves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/TextureLeaves.png -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Resources/TextureSand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/TextureSand.png -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Resources/TextureStone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/TextureStone.png -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Resources/TextureWater.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/TextureWater.png -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Resources/TextureWood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks.swiftpm/Resources/TextureWood.png -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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.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.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/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/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/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/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/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.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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/Model/Block.swift: -------------------------------------------------------------------------------- 1 | /// A block in the world without a position. 2 | struct Block: Hashable, Codable { 3 | let type: BlockType 4 | } 5 | -------------------------------------------------------------------------------- /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/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.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/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.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/Model/InventoryConstants.swift: -------------------------------------------------------------------------------- 1 | enum InventoryConstants { 2 | static let inventorySlotCount = 24 3 | static let hotbarSlotCount = 8 4 | } 5 | -------------------------------------------------------------------------------- /MiniBlocks.swiftpm/Sources/Model/Item.swift: -------------------------------------------------------------------------------- 1 | /// An inventory item. 2 | struct Item: Hashable, Codable { 3 | let type: ItemType 4 | } 5 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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.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.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 | -------------------------------------------------------------------------------- /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/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.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/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/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/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/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/ChunkConstants.swift: -------------------------------------------------------------------------------- 1 | enum ChunkConstants { 2 | static let size: Int = 8 3 | } 4 | -------------------------------------------------------------------------------- /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/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/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/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/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/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.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/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/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/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/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/Utils/SceneKitUtils.swift: -------------------------------------------------------------------------------- 1 | import SceneKit 2 | 3 | // Adds the missing operator overloads to SceneKit vectors. 4 | extension SCNVector3: Vec3Protocol {} 5 | 6 | -------------------------------------------------------------------------------- /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.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/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.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.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.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/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/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.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/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/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 | -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/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/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/MiniBlocks/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocks/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /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/MiniBlocks/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/MiniBlocks/MiniBlocksToGo/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /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/MiniBlocksToGo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniBlocks 2 | 3 | [![Build](https://github.com/fwcd/mini-blocks/actions/workflows/build.yml/badge.svg)](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 | ![Showcase](showcase.png) 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 | -------------------------------------------------------------------------------- /showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/mini-blocks/76ab35a279131345f3bc429b0c52b697749af976/showcase.png --------------------------------------------------------------------------------