diff --git a/SDPhysicsEngine/Sources/SDPhysicsEngine/GameScene.swift b/SDPhysicsEngine/Sources/SDPhysicsEngine/GameScene.swift index c002f9e7..f09e396c 100644 --- a/SDPhysicsEngine/Sources/SDPhysicsEngine/GameScene.swift +++ b/SDPhysicsEngine/Sources/SDPhysicsEngine/GameScene.swift @@ -8,6 +8,8 @@ public class GameScene: SKScene { private var objectMap: [SKNode: SDObject] = [:] + private var cameraPlayerMap: [Int: SDCameraObject] = [:] + override public func sceneDidLoad() { super.sceneDidLoad() @@ -21,15 +23,38 @@ public class GameScene: SKScene { lastUpdateTime = currentTime return } - let deltaTime = currentTime - lastUpdateTime self.lastUpdateTime = currentTime + self.updateCameras() sceneDelegate?.update(self, deltaTime: deltaTime) } + + func updateCameras() { + for camera in cameraPlayerMap.values { + camera.update() + } + } + + public func useCamera(of playerIndex: Int, rotatedBy rotation: CGFloat) { + guard let cameraObject = cameraPlayerMap[playerIndex] else { + return + } + + cameraObject.zRotation = rotation + self.camera = cameraObject.cameraNode + } } extension GameScene: SDScene { + public func addPlayerObject(_ playerObject: SDObject, playerIndex: Int) { + let camera = SDCameraObject(player: playerObject) + cameraPlayerMap[playerIndex] = camera + + addObject(camera) + addObject(playerObject) + } + public func addObject(_ object: SDObject) { guard objectMap[object.node] == nil else { return @@ -43,15 +68,6 @@ extension GameScene: SDScene { objectMap[object.node] = nil object.removeFromParent() } - - public func addCameraObject(_ cameraObject: SDCameraObject) { - addObject(cameraObject) - camera = cameraObject.cameraNode - } - - public func setCameraObjectXPosition(to x: CGFloat) { - camera?.position.x = x - } } extension GameScene: SKPhysicsContactDelegate { diff --git a/SDPhysicsEngine/Sources/SDPhysicsEngine/Object/SDCameraObject.swift b/SDPhysicsEngine/Sources/SDPhysicsEngine/Object/SDCameraObject.swift index 320069ef..5dcdf1e9 100644 --- a/SDPhysicsEngine/Sources/SDPhysicsEngine/Object/SDCameraObject.swift +++ b/SDPhysicsEngine/Sources/SDPhysicsEngine/Object/SDCameraObject.swift @@ -2,9 +2,29 @@ import SpriteKit public class SDCameraObject: SDObject { let cameraNode: SKCameraNode + let player: SDObject? override public init() { + player = nil cameraNode = SKCameraNode() super.init(node: cameraNode) } + + init(player: SDObject) { + self.player = player + cameraNode = SKCameraNode() + super.init(node: cameraNode) + } + + var zRotation: CGFloat { + get { cameraNode.zRotation } + set { cameraNode.zRotation = newValue } + } + + func update() { + guard let player = self.player else { + return + } + self.position = player.position + } } diff --git a/SDPhysicsEngine/Sources/SDPhysicsEngine/SDScene.swift b/SDPhysicsEngine/Sources/SDPhysicsEngine/SDScene.swift index 893c9312..cd1efa90 100644 --- a/SDPhysicsEngine/Sources/SDPhysicsEngine/SDScene.swift +++ b/SDPhysicsEngine/Sources/SDPhysicsEngine/SDScene.swift @@ -4,8 +4,7 @@ public protocol SDScene { var size: CGSize { get } + func addPlayerObject(_ playerObject: SDObject, playerIndex: Int) func addObject(_ object: SDObject) func removeObject(_ object: SDObject) - func addCameraObject(_ cameraObject: SDCameraObject) - func setCameraObjectXPosition(to x: CGFloat) } diff --git a/star-dash/star-dash.xcodeproj/project.pbxproj b/star-dash/star-dash.xcodeproj/project.pbxproj index e2a3c7bd..12a4ba8b 100644 --- a/star-dash/star-dash.xcodeproj/project.pbxproj +++ b/star-dash/star-dash.xcodeproj/project.pbxproj @@ -55,7 +55,6 @@ 46D418242BA5D5280091A38B /* Tool+Collidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D418232BA5D5280091A38B /* Tool+Collidable.swift */; }; 46D418262BA5D6500091A38B /* Wall+Collidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D418252BA5D6500091A38B /* Wall+Collidable.swift */; }; 46D418282BA5D6800091A38B /* Floor+Collidable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D418272BA5D6800091A38B /* Floor+Collidable.swift */; }; - 4E0905FD2BB4A15600DE666B /* MiniMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0905FC2BB4A15600DE666B /* MiniMapView.swift */; }; 4E0905FF2BB4A4EB00DE666B /* PlayerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E0905FE2BB4A4EB00DE666B /* PlayerInfo.swift */; }; 4E3458DF2BA7490B00E817C6 /* EntityManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3458DE2BA7490B00E817C6 /* EntityManagerTests.swift */; }; 4E3458E52BA75E6800E817C6 /* PositionComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3458E42BA75E6800E817C6 /* PositionComponentTests.swift */; }; @@ -108,6 +107,8 @@ E64361132BA4C2CD003850FD /* PhysicsModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = E643610D2BA4C2CC003850FD /* PhysicsModule.swift */; }; E64361142BA4C2CD003850FD /* CreationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = E643610E2BA4C2CC003850FD /* CreationModule.swift */; }; E64361152BA4C2CD003850FD /* GameBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = E643610F2BA4C2CC003850FD /* GameBridge.swift */; }; + E64F31672BB3D14600EC5371 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E64F31662BB3D14600EC5371 /* UIView.swift */; }; + E660353D2BB5EAEE00397068 /* MiniMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E660353C2BB5EAEE00397068 /* MiniMapView.swift */; }; E69EE9322BAC6CBB00033AB5 /* GameInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69EE9312BAC6CBB00033AB5 /* GameInfo.swift */; }; E69EE9342BAC6CC300033AB5 /* OverlayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69EE9332BAC6CC300033AB5 /* OverlayInfo.swift */; }; E69FDDE02BAD3DAD0089D5F3 /* PointsComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69FDDDF2BAD3DAD0089D5F3 /* PointsComponent.swift */; }; @@ -115,6 +116,8 @@ E69FDDE42BADC2F10089D5F3 /* SpriteConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69FDDE32BADC2F10089D5F3 /* SpriteConstants.swift */; }; E69FDDE62BADD11B0089D5F3 /* StopMovingEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69FDDE52BADD11B0089D5F3 /* StopMovingEvent.swift */; }; E6A011172BA5F4AD006904D9 /* EntitySyncInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A011162BA5F4AD006904D9 /* EntitySyncInterface.swift */; }; + E6A72FC52BB2E1E10015729E /* PlayerViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A72FC32BB2E1E10015729E /* PlayerViewLayout.swift */; }; + E6A72FC62BB2E1E10015729E /* LayoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A72FC42BB2E1E10015729E /* LayoutUtils.swift */; }; E6A745162BA057040080C1BE /* MTKRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A745112BA057040080C1BE /* MTKRenderer.swift */; }; E6A745172BA057040080C1BE /* ControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A745122BA057040080C1BE /* ControlView.swift */; }; E6A745182BA057040080C1BE /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A745132BA057040080C1BE /* Renderer.swift */; }; @@ -263,6 +266,8 @@ E643610D2BA4C2CC003850FD /* PhysicsModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhysicsModule.swift; sourceTree = ""; }; E643610E2BA4C2CC003850FD /* CreationModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreationModule.swift; sourceTree = ""; }; E643610F2BA4C2CC003850FD /* GameBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameBridge.swift; sourceTree = ""; }; + E64F31662BB3D14600EC5371 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; + E660353C2BB5EAEE00397068 /* MiniMapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiniMapView.swift; sourceTree = ""; }; E69EE9312BAC6CBB00033AB5 /* GameInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameInfo.swift; sourceTree = ""; }; E69EE9332BAC6CC300033AB5 /* OverlayInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverlayInfo.swift; sourceTree = ""; }; E69FDDDF2BAD3DAD0089D5F3 /* PointsComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PointsComponent.swift; sourceTree = ""; }; @@ -270,6 +275,8 @@ E69FDDE32BADC2F10089D5F3 /* SpriteConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpriteConstants.swift; sourceTree = ""; }; E69FDDE52BADD11B0089D5F3 /* StopMovingEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StopMovingEvent.swift; sourceTree = ""; }; E6A011162BA5F4AD006904D9 /* EntitySyncInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntitySyncInterface.swift; sourceTree = ""; }; + E6A72FC32BB2E1E10015729E /* PlayerViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerViewLayout.swift; sourceTree = ""; }; + E6A72FC42BB2E1E10015729E /* LayoutUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutUtils.swift; sourceTree = ""; }; E6A745112BA057040080C1BE /* MTKRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MTKRenderer.swift; sourceTree = ""; }; E6A745122BA057040080C1BE /* ControlView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlView.swift; sourceTree = ""; }; E6A745132BA057040080C1BE /* Renderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = ""; }; @@ -315,6 +322,7 @@ isa = PBXGroup; children = ( 143AA38E2BA4D61A009C28E7 /* CGVector.swift */, + E64F31662BB3D14600EC5371 /* UIView.swift */, ); path = Extensions; sourceTree = ""; @@ -507,6 +515,7 @@ 142D9F8E2BA15FBB005FE9E0 /* .swiftlint.yml */, 4E630EF32B9F7E070008F887 /* Products */, E6B1DC912BA34A5E00473563 /* Frameworks */, + E660353B2BB5EAD300397068 /* Recovered References */, ); sourceTree = ""; }; @@ -636,6 +645,27 @@ path = SyncModule; sourceTree = ""; }; + E64F31682BB400CB00EC5371 /* PlayerView */ = { + isa = PBXGroup; + children = ( + E660353C2BB5EAEE00397068 /* MiniMapView.swift */, + E6A7451A2BA0C1890080C1BE /* PlayerView.swift */, + E6A7451C2BA0CAD90080C1BE /* JoystickView.swift */, + E6A745122BA057040080C1BE /* ControlView.swift */, + E6B550A02BA15E9C00DC7396 /* OverlayView.swift */, + E6B0AAD22BAAE438009CB939 /* ControlViewDelegate.swift */, + ); + path = PlayerView; + sourceTree = ""; + }; + E660353B2BB5EAD300397068 /* Recovered References */ = { + isa = PBXGroup; + children = ( + 4E0905FC2BB4A15600DE666B /* MiniMapView.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; E6A011152BA5D111006904D9 /* GameBridge */ = { isa = PBXGroup; children = ( @@ -646,14 +676,24 @@ path = GameBridge; sourceTree = ""; }; + E6A72FC22BB2E1E10015729E /* Utils */ = { + isa = PBXGroup; + children = ( + E6A72FC32BB2E1E10015729E /* PlayerViewLayout.swift */, + E6A72FC42BB2E1E10015729E /* LayoutUtils.swift */, + ); + path = Utils; + sourceTree = ""; + }; E6A745102BA057040080C1BE /* Rendering */ = { isa = PBXGroup; children = ( - E69EE9332BAC6CC300033AB5 /* OverlayInfo.swift */, - E6B5509F2BA15D2000DC7396 /* MTKRenderer */, + E6A72FC22BB2E1E10015729E /* Utils */, + E64F31682BB400CB00EC5371 /* PlayerView */, E6A745132BA057040080C1BE /* Renderer.swift */, + E6A745112BA057040080C1BE /* MTKRenderer.swift */, E6B0AAD02BAAE3DC009CB939 /* ViewDelegate.swift */, - E6B0AAD22BAAE438009CB939 /* ControlViewDelegate.swift */, + E69EE9332BAC6CC300033AB5 /* OverlayInfo.swift */, ); path = Rendering; sourceTree = ""; @@ -665,19 +705,6 @@ name = Frameworks; sourceTree = ""; }; - E6B5509F2BA15D2000DC7396 /* MTKRenderer */ = { - isa = PBXGroup; - children = ( - E6A745112BA057040080C1BE /* MTKRenderer.swift */, - E6A7451A2BA0C1890080C1BE /* PlayerView.swift */, - E6A7451C2BA0CAD90080C1BE /* JoystickView.swift */, - E6A745122BA057040080C1BE /* ControlView.swift */, - E6B550A02BA15E9C00DC7396 /* OverlayView.swift */, - 4E0905FC2BB4A15600DE666B /* MiniMapView.swift */, - ); - path = MTKRenderer; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -870,11 +897,11 @@ E64361122BA4C2CD003850FD /* ObjectModule.swift in Sources */, 4E0905FF2BB4A4EB00DE666B /* PlayerInfo.swift in Sources */, 4E59E2532BAB3061007B3FA7 /* Database.swift in Sources */, + E6A72FC52BB2E1E10015729E /* PlayerViewLayout.swift in Sources */, 4E8660662BA097D40035530D /* PhysicsConstants.swift in Sources */, 143AA3952BA4DF1C009C28E7 /* MonsterAttackPlayerEvent.swift in Sources */, 143AA3932BA4DBE7009C28E7 /* PlayerMonsterContactEvent.swift in Sources */, 4E59E2682BADA7B6007B3FA7 /* CollectibleEntityPersistable.swift in Sources */, - 4E0905FD2BB4A15600DE666B /* MiniMapView.swift in Sources */, 46D418162BA5CBD60091A38B /* Collidable.swift in Sources */, 461148912BA1CDBF0073E7E1 /* SystemManager.swift in Sources */, 460A20112BB1E6DF002597B8 /* MonsterSystem.swift in Sources */, @@ -899,6 +926,7 @@ 4604BBD92BA81C940078B84C /* InventorySystem.swift in Sources */, 14E247952BA2CB480071FFC0 /* EventManager.swift in Sources */, 4EC561E32BB1E98400166DDC /* PlayerObstacleContactEvent.swift in Sources */, + E660353D2BB5EAEE00397068 /* MiniMapView.swift in Sources */, 4E630EF82B9F7E070008F887 /* SceneDelegate.swift in Sources */, 46C3B1C22BB467BB00F10574 /* EntityManagerInterface.swift in Sources */, E69FDDE02BAD3DAD0089D5F3 /* PointsComponent.swift in Sources */, @@ -915,6 +943,7 @@ 46C3B1C42BB5055F00F10574 /* EntityBuilder.swift in Sources */, 4E59E2612BAB42FD007B3FA7 /* LevelPersistable.swift in Sources */, 460A200B2BB1D04F002597B8 /* AttackSystem.swift in Sources */, + E6A72FC62BB2E1E10015729E /* LayoutUtils.swift in Sources */, E6A745162BA057040080C1BE /* MTKRenderer.swift in Sources */, E6A745182BA057040080C1BE /* Renderer.swift in Sources */, 4E59E25F2BAB4134007B3FA7 /* EntityType.swift in Sources */, @@ -933,6 +962,7 @@ 4E630F372B9F91DE0008F887 /* PlayerSprite.swift in Sources */, 14970F502BA814D500CC1E8A /* ScoreComponent.swift in Sources */, E69FDDE42BADC2F10089D5F3 /* SpriteConstants.swift in Sources */, + E64F31672BB3D14600EC5371 /* UIView.swift in Sources */, 143AA3912BA4D7AC009C28E7 /* MonsterDeathEvent.swift in Sources */, E64361102BA4C2CD003850FD /* SpriteModule.swift in Sources */, 4604BBD52BA819C70078B84C /* InventoryComponent.swift in Sources */, diff --git a/star-dash/star-dash/Extensions/UIView.swift b/star-dash/star-dash/Extensions/UIView.swift new file mode 100644 index 00000000..0762e97e --- /dev/null +++ b/star-dash/star-dash/Extensions/UIView.swift @@ -0,0 +1,13 @@ +import UIKit + +/** + * The extension introduces an initisalisation method to add subviews + * to a frame that is rotated, however the view is not rotated. + */ +extension UIView { + convenience init(frame: CGRect, rotatedBy rotation: CGFloat) { + self.init(frame: frame.applying(CGAffineTransform(rotationAngle: rotation))) + self.transform = CGAffineTransform(rotationAngle: rotation) + self.center = CGPoint(x: frame.midX, y: frame.midY) + } +} diff --git a/star-dash/star-dash/GameBridge/GameBridge.swift b/star-dash/star-dash/GameBridge/GameBridge.swift index f93f90a8..e5cca642 100644 --- a/star-dash/star-dash/GameBridge/GameBridge.swift +++ b/star-dash/star-dash/GameBridge/GameBridge.swift @@ -85,7 +85,14 @@ class GameBridge { modules.forEach { $0.create(for: newObject, from: entity) } - self.scene.addObject(newObject) + + if type(of: entity) == Player.self { + if let playerIndex = entityManager.component(ofType: PlayerComponent.self, of: entity.id)?.playerIndex { + self.scene.addPlayerObject(newObject, playerIndex: playerIndex) + } + } else { + self.scene.addObject(newObject) + } } private func removeObject(from entityId: EntityId) { diff --git a/star-dash/star-dash/GameEngine/Entities/EntityManager.swift b/star-dash/star-dash/GameEngine/Entities/EntityManager.swift index 61b61839..ea337912 100644 --- a/star-dash/star-dash/GameEngine/Entities/EntityManager.swift +++ b/star-dash/star-dash/GameEngine/Entities/EntityManager.swift @@ -73,10 +73,12 @@ class EntityManager { entityMap[entityId] } - func playerEntityId() -> EntityId? { - // TODO: Add parameter to specify the player index - for entityId in entityMap.keys where component(ofType: PlayerComponent.self, of: entityId) != nil { - return entityId + func playerEntityId(with playerIndex: Int) -> EntityId? { + for entityId in entityMap.keys { + if let playerComponent = component(ofType: PlayerComponent.self, of: entityId), + playerComponent.playerIndex == playerIndex { + return entityId + } } return nil diff --git a/star-dash/star-dash/GameEngine/GameEngine.swift b/star-dash/star-dash/GameEngine/GameEngine.swift index 9eb1975f..d29b6a5c 100644 --- a/star-dash/star-dash/GameEngine/GameEngine.swift +++ b/star-dash/star-dash/GameEngine/GameEngine.swift @@ -20,21 +20,21 @@ class GameEngine { setUpSystems() } - func gameInfo() -> GameInfo? { + func gameInfo(forPlayer playerIndex: Int) -> GameInfo? { guard let scoreSystem = systemManager.system(ofType: ScoreSystem.self), - let playerEntityId = entityManager.playerEntityId(), + let playerEntityId = entityManager.playerEntityId(with: playerIndex), let score = scoreSystem.score(of: playerEntityId) else { return nil } return GameInfo( playerScore: score, - playersInfo: playersInfo() + playersInfo: playersInfo(of: playerIndex) ) } - func playersInfo() -> [PlayerInfo] { - guard let playerEntityId = entityManager.playerEntityId(), + func playersInfo(of playerIndex: Int) -> [PlayerInfo] { + guard let playerEntityId = entityManager.playerEntityId(with: playerIndex), let positionSystem = systemManager.system(ofType: PositionSystem.self) else { return [] } @@ -61,16 +61,16 @@ class GameEngine { eventManager.add(event: event) } - func handlePlayerJump() { - guard let playerEntityId = entityManager.playerEntityId() else { + func handlePlayerJump(playerIndex: Int) { + guard let playerEntityId = entityManager.playerEntityId(with: playerIndex) else { return } eventManager.add(event: JumpEvent(on: playerEntityId, by: PhysicsConstants.jumpImpulse)) } - func handlePlayerMove(toLeft: Bool) { - guard let playerEntityId = entityManager.playerEntityId(), + func handlePlayerMove(toLeft: Bool, playerIndex: Int) { + guard let playerEntityId = entityManager.playerEntityId(with: playerIndex), let playerComponent = entityManager.component(ofType: PlayerComponent.self, of: playerEntityId), playerComponent.canMove else { return @@ -79,22 +79,14 @@ class GameEngine { eventManager.add(event: MoveEvent(on: playerEntityId, toLeft: toLeft)) } - func handlePlayerStoppedMoving() { - guard let playerEntityId = entityManager.playerEntityId() else { + func handlePlayerStoppedMoving(playerIndex: Int) { + guard let playerEntityId = entityManager.playerEntityId(with: playerIndex) else { return } eventManager.add(event: StopMovingEvent(on: playerEntityId)) } - func playerPosition() -> CGPoint? { - guard let playerEntityId = entityManager.playerEntityId(), - let positionComponent = entityManager.component(ofType: PositionComponent.self, of: playerEntityId) else { - return nil - } - return positionComponent.position - } - private func setUpSystems() { systemManager.add(PositionSystem(entityManager, dispatcher: self)) systemManager.add(PhysicsSystem(entityManager, dispatcher: self)) diff --git a/star-dash/star-dash/Rendering/ControlViewDelegate.swift b/star-dash/star-dash/Rendering/ControlViewDelegate.swift deleted file mode 100644 index 0184c992..00000000 --- a/star-dash/star-dash/Rendering/ControlViewDelegate.swift +++ /dev/null @@ -1,6 +0,0 @@ -protocol ControlViewDelegate: AnyObject { - - func joystickMoved(toLeft: Bool) - func joystickReleased() - func jumpButtonPressed() -} diff --git a/star-dash/star-dash/Rendering/MTKRenderer.swift b/star-dash/star-dash/Rendering/MTKRenderer.swift new file mode 100644 index 00000000..11e9bcbf --- /dev/null +++ b/star-dash/star-dash/Rendering/MTKRenderer.swift @@ -0,0 +1,134 @@ +import UIKit +import SpriteKit +import MetalKit +import SDPhysicsEngine + +/** + `MTKRenderer` is a `Renderer` that uses MetalKit and SpriteKit to render + the game on to the iOS device. + + The `SKScene` is rendered through a MetalKit while the controls + and game information overlay is rendered through UIKit. + */ +class MTKRenderer: NSObject, Renderer { + var scene: GameScene + var device: MTLDevice + var commandQueue: MTLCommandQueue + + var renderer: SKRenderer + + var playerViews: [PlayerView] + + var viewDelegate: ViewDelegate? + + init?(scene: GameScene) { + self.scene = scene + + guard let device = MTLCreateSystemDefaultDevice(), + let commandQueue = device.makeCommandQueue() else { + return nil + } + self.playerViews = [] + self.device = device + self.commandQueue = commandQueue + self.renderer = SKRenderer(device: device) + renderer.scene = scene + + super.init() + } + + func setupViews(at superview: UIView, for numberOfPlayers: Int) { + guard let layouts = LayoutUtils.layoutViews(superview: superview, for: numberOfPlayers) else { + return + } + + for layout in layouts { + let playerView = PlayerView.createPlayerView(layout: layout, device: self.device) + playerView.setControlViewDelegate(self) + playerView.setDrawDelegate(self) + self.playerViews.append(playerView) + } + } + + private func playerIndex(from mtkView: MTKView) -> Int? { + for i in 0.. Int? { + for i in 0.., with event: UIEvent?) { @@ -80,7 +80,7 @@ class ControlView: UIView, UIGestureRecognizerDelegate { // controlViewDelegate?.joystickMoved(toLeft: isLeft) // } let isLeft = firstTouch.location(in: joystickView).x < joystickView.center.x - controlViewDelegate?.joystickMoved(toLeft: isLeft) + controlViewDelegate?.joystickMoved(toLeft: isLeft, from: self) joystickView.moveJoystick(location: firstTouch.location(in: joystickView)) } @@ -93,7 +93,7 @@ class ControlView: UIView, UIGestureRecognizerDelegate { return } - controlViewDelegate?.joystickReleased() + controlViewDelegate?.joystickReleased(from: self) joystickView?.returnJoystick() } // To ensure gesture recognise only in a specific area @@ -115,7 +115,7 @@ class ControlView: UIView, UIGestureRecognizerDelegate { let location = gesture.location(in: self) if gesture.state == .ended { - controlViewDelegate?.joystickReleased() + controlViewDelegate?.joystickReleased(from: self) joystickView.returnJoystick() return } @@ -123,7 +123,7 @@ class ControlView: UIView, UIGestureRecognizerDelegate { joystickView.moveJoystick(location: gesture.location(in: joystickView)) if shouldSendMoveEvent(location: location) { let isLeft = gesture.location(in: joystickView).x < joystickView.center.x - controlViewDelegate?.joystickMoved(toLeft: isLeft) + controlViewDelegate?.joystickMoved(toLeft: isLeft, from: self) } } diff --git a/star-dash/star-dash/Rendering/PlayerView/ControlViewDelegate.swift b/star-dash/star-dash/Rendering/PlayerView/ControlViewDelegate.swift new file mode 100644 index 00000000..65c57091 --- /dev/null +++ b/star-dash/star-dash/Rendering/PlayerView/ControlViewDelegate.swift @@ -0,0 +1,6 @@ +protocol ControlViewDelegate: AnyObject { + + func joystickMoved(toLeft: Bool, from view: ControlView) + func joystickReleased(from view: ControlView) + func jumpButtonPressed(from view: ControlView) +} diff --git a/star-dash/star-dash/Rendering/MTKRenderer/JoystickView.swift b/star-dash/star-dash/Rendering/PlayerView/JoystickView.swift similarity index 100% rename from star-dash/star-dash/Rendering/MTKRenderer/JoystickView.swift rename to star-dash/star-dash/Rendering/PlayerView/JoystickView.swift diff --git a/star-dash/star-dash/Rendering/MTKRenderer/MiniMapView.swift b/star-dash/star-dash/Rendering/PlayerView/MiniMapView.swift similarity index 100% rename from star-dash/star-dash/Rendering/MTKRenderer/MiniMapView.swift rename to star-dash/star-dash/Rendering/PlayerView/MiniMapView.swift diff --git a/star-dash/star-dash/Rendering/MTKRenderer/OverlayView.swift b/star-dash/star-dash/Rendering/PlayerView/OverlayView.swift similarity index 93% rename from star-dash/star-dash/Rendering/MTKRenderer/OverlayView.swift rename to star-dash/star-dash/Rendering/PlayerView/OverlayView.swift index dd49363e..f62ed778 100644 --- a/star-dash/star-dash/Rendering/MTKRenderer/OverlayView.swift +++ b/star-dash/star-dash/Rendering/PlayerView/OverlayView.swift @@ -22,10 +22,10 @@ class OverlayView: UIView { scoreLabel.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor, constant: margin) ]) - update(score: 0) + update(0) } - func update(score: Int) { + func update(_ score: Int) { scoreLabel.text = "Score: \(score)" } } diff --git a/star-dash/star-dash/Rendering/PlayerView/PlayerView.swift b/star-dash/star-dash/Rendering/PlayerView/PlayerView.swift new file mode 100644 index 00000000..df60bfa2 --- /dev/null +++ b/star-dash/star-dash/Rendering/PlayerView/PlayerView.swift @@ -0,0 +1,65 @@ +import UIKit +import MetalKit +import CoreGraphics + +/** + `PlayerView` is responsible for creating and coordinating the views + for a specific player. + + It consists of 3 views: + 1. Scene View: The game from the player's perspective + 2. Control View: The controls the player interacts with + 3. Overlay View: The game overlay to show information such as points + */ +class PlayerView { + let superview: UIView + + var sceneView: MTKView + var controlView: ControlView + var overlayView: OverlayView + var minimapView: MiniMapView + + let rotation: CGFloat + + private init(superview: UIView, rotation: CGFloat, device: MTLDevice) { + self.rotation = rotation + self.superview = superview + + self.sceneView = MTKView(frame: superview.bounds, device: device) + superview.addSubview(self.sceneView) + + self.overlayView = OverlayView(frame: superview.bounds, rotatedBy: rotation) + superview.addSubview(self.overlayView) + + self.minimapView = MiniMapView(frame: superview.bounds, rotatedBy: rotation) + superview.addSubview(self.minimapView) + + self.controlView = ControlView(frame: superview.bounds, rotatedBy: rotation) + superview.addSubview(self.controlView) + } + + func setupSubviews() { + self.controlView.setupSubviews() + self.overlayView.setupSubviews() + self.minimapView.setupSubviews() + } + + func setControlViewDelegate(_ delegate: ControlViewDelegate) { + controlView.controlViewDelegate = delegate + } + + func setDrawDelegate(_ delegate: MTKViewDelegate) { + sceneView.delegate = delegate + } + + func update(_ overlayInfo: OverlayInfo) { + overlayView.update(overlayInfo.score) + minimapView.update(playersInfo: overlayInfo.playersInfo) + } + + static func createPlayerView(layout: PlayerViewLayout, device: MTLDevice) -> PlayerView { + let playerView = PlayerView(superview: layout.superview, rotation: layout.rotation, device: device) + playerView.setupSubviews() + return playerView + } +} diff --git a/star-dash/star-dash/Rendering/Renderer.swift b/star-dash/star-dash/Rendering/Renderer.swift index 1cd7c11c..f2ddabd4 100644 --- a/star-dash/star-dash/Rendering/Renderer.swift +++ b/star-dash/star-dash/Rendering/Renderer.swift @@ -4,6 +4,5 @@ import UIKit The `Renderer` protocol defines the requirements for an object responsible for rendering game objects onto a view. */ protocol Renderer { - func updateOverlay(overlayInfo: OverlayInfo) - func createSinglePlayerView(at rootView: UIView) + func setupViews(at rootView: UIView, for numberOfPlayers: Int) } diff --git a/star-dash/star-dash/Rendering/Utils/LayoutUtils.swift b/star-dash/star-dash/Rendering/Utils/LayoutUtils.swift new file mode 100644 index 00000000..045e28e3 --- /dev/null +++ b/star-dash/star-dash/Rendering/Utils/LayoutUtils.swift @@ -0,0 +1,43 @@ +import UIKit + +class LayoutUtils { + + static func layoutViews(superview: UIView, for numberOfPlayers: Int) -> [PlayerViewLayout]? { + let layouts: [Int: (UIView) -> [PlayerViewLayout]] = [ + 1: createLayoutForSinglePlayer, + 2: createLayoutForTwoPlayers + ] + + guard let layoutMethod = layouts[numberOfPlayers] else { + return nil + } + + return layoutMethod(superview) + } + + static func createLayoutForSinglePlayer(superview: UIView) -> [PlayerViewLayout] { + [PlayerViewLayout( + superview: superview, + rotation: 0 + )] + } + + static func createLayoutForTwoPlayers(superview: UIView) -> [PlayerViewLayout] { + let player1View = UIView() + let player2View = UIView() + + superview.addSubview(player1View) + superview.addSubview(player2View) + + player1View.frame = CGRect(x: 0, y: 0, width: superview.frame.width / 2, height: superview.frame.height) + player2View.frame = CGRect(x: superview.frame.width / 2, + y: 0, + width: superview.frame.width / 2, + height: superview.frame.height) + + return [ + PlayerViewLayout(superview: player1View, rotation: .pi / 2), + PlayerViewLayout(superview: player2View, rotation: .pi * 3 / 2) + ] + } +} diff --git a/star-dash/star-dash/Rendering/Utils/PlayerViewLayout.swift b/star-dash/star-dash/Rendering/Utils/PlayerViewLayout.swift new file mode 100644 index 00000000..4ecaa92f --- /dev/null +++ b/star-dash/star-dash/Rendering/Utils/PlayerViewLayout.swift @@ -0,0 +1,6 @@ +import UIKit + +struct PlayerViewLayout { + let superview: UIView + let rotation: CGFloat +} diff --git a/star-dash/star-dash/Rendering/ViewDelegate.swift b/star-dash/star-dash/Rendering/ViewDelegate.swift index 05195180..15e16ecc 100644 --- a/star-dash/star-dash/Rendering/ViewDelegate.swift +++ b/star-dash/star-dash/Rendering/ViewDelegate.swift @@ -1,6 +1,7 @@ protocol ViewDelegate: AnyObject { - func joystickMoved(toLeft: Bool) - func joystickReleased() - func jumpButtonPressed() + func joystickMoved(toLeft: Bool, playerIndex: Int) + func joystickReleased(playerIndex: Int) + func jumpButtonPressed(playerIndex: Int) + func overlayInfo(forPlayer playerIndex: Int) -> OverlayInfo? } diff --git a/star-dash/star-dash/ViewController.swift b/star-dash/star-dash/ViewController.swift index 82be0754..66680b09 100644 --- a/star-dash/star-dash/ViewController.swift +++ b/star-dash/star-dash/ViewController.swift @@ -33,7 +33,7 @@ class ViewController: UIViewController { } renderer.viewDelegate = self - renderer.createSinglePlayerView(at: self.view) + renderer.setupViews(at: self.view, for: 2) self.renderer = renderer } @@ -43,10 +43,6 @@ class ViewController: UIViewController { return } - let camera = SDCameraObject() - camera.position = CGPoint(x: scene.size.width / 2, y: scene.size.height / 2) - scene.addCameraObject(camera) - let background = SDSpriteObject(imageNamed: "GameBackground") background.position = CGPoint(x: scene.size.width / 2, y: scene.size.height / 2) background.zPosition = -1 @@ -56,6 +52,10 @@ class ViewController: UIViewController { playerIndex: 0, position: CGPoint(x: 100, y: scene.size.height / 2 + 200)) + EntityFactory.createAndAddPlayer(to: gameEngine, + playerIndex: 1, + position: CGPoint(x: scene.size.width / 2, y: scene.size.height / 2 + 200)) + EntityFactory.createAndAddFloor(to: gameEngine, position: CGPoint(x: scene.size.width / 2, y: scene.size.height / 2 - 400), size: CGSize(width: 8_000, height: 10)) @@ -76,32 +76,6 @@ extension ViewController: SDSceneDelegate { gameBridge?.syncToEntities() gameEngine?.update(by: deltaTime) gameBridge?.syncFromEntities() - - updateCameraObjectPosition(scene) - updateOverlay() - } - - private func updateCameraObjectPosition(_ scene: SDScene) { - guard let playerPosition = gameEngine?.playerPosition() else { - return - } - let screenSize = UIScreen.main.bounds.size - let halfScreenWidth = screenSize.width / 2 - if playerPosition.x >= halfScreenWidth + 200 { - scene.setCameraObjectXPosition(to: playerPosition.x) - } else { - scene.setCameraObjectXPosition(to: halfScreenWidth + 200) - } - } - - private func updateOverlay() { - guard let gameInfo = gameEngine?.gameInfo() else { - return - } - renderer?.updateOverlay(overlayInfo: OverlayInfo( - score: gameInfo.playerScore, - playersInfo: gameInfo.playersInfo - )) } func contactOccurred(objectA: SDObject, objectB: SDObject, contactPoint: CGPoint) { @@ -116,15 +90,26 @@ extension ViewController: SDSceneDelegate { extension ViewController: ViewDelegate { - func joystickMoved(toLeft: Bool) { - gameEngine?.handlePlayerMove(toLeft: toLeft) + func joystickMoved(toLeft: Bool, playerIndex: Int) { + gameEngine?.handlePlayerMove(toLeft: toLeft, playerIndex: playerIndex) + } + + func joystickReleased(playerIndex: Int) { + gameEngine?.handlePlayerStoppedMoving(playerIndex: playerIndex) } - func joystickReleased() { - gameEngine?.handlePlayerStoppedMoving() + func jumpButtonPressed(playerIndex: Int) { + gameEngine?.handlePlayerJump(playerIndex: playerIndex) } - func jumpButtonPressed() { - gameEngine?.handlePlayerJump() + func overlayInfo(forPlayer playerIndex: Int) -> OverlayInfo? { + guard let gameInfo = gameEngine?.gameInfo(forPlayer: playerIndex) else { + return nil + } + + return OverlayInfo( + score: gameInfo.playerScore, + playersInfo: gameInfo.playersInfo + ) } }