Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Systems #4

Merged
merged 13 commits into from Mar 15, 2024
177 changes: 132 additions & 45 deletions star-dash/star-dash.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb",
"version" : "1.1.0"
}
}
],
"version" : 2
}
15 changes: 15 additions & 0 deletions star-dash/star-dash/Events/Event.swift
@@ -0,0 +1,15 @@
//
// Event.swift
// star-dash
//
// Created by Jason Qiu on 13/3/24.
//

import Foundation

protocol Event {
var timestamp: Date { get }
var entityId: EntityId { get }

func execute(on target: EventModifiable)
}
29 changes: 29 additions & 0 deletions star-dash/star-dash/Events/EventManager.swift
@@ -0,0 +1,29 @@
//
// EventManager.swift
// star-dash
//
// Created by Jason Qiu on 14/3/24.
//

import DequeModule
import Foundation

typealias EventQueue = Deque<Event>

class EventManager {
private var events: EventQueue

init() {
events = EventQueue()
}

func add(event: Event) {
events.append(event)
}

func executeAll(on target: EventModifiable) {
while let event = events.popFirst() {
event.execute(on: target)
}
}
}
11 changes: 11 additions & 0 deletions star-dash/star-dash/Events/EventModifiable.swift
@@ -0,0 +1,11 @@
//
// EventModifiable.swift
// star-dash
//
// Created by Jason Qiu on 14/3/24.
//

import Foundation

/// EventModifiable represents objects that can be modified by events.
protocol EventModifiable { }
25 changes: 25 additions & 0 deletions star-dash/star-dash/Events/MoveEvent.swift
@@ -0,0 +1,25 @@
//
// MoveEvent.swift
// star-dash
//
// Created by Jason Qiu on 14/3/24.
//

import Foundation

class MoveEvent: Event {
var entityId: EntityId
var timestamp: Date

var displacement: CGVector

init(entityId: EntityId, displacement: CGVector, timestamp: Date = Date.now) {
self.entityId = entityId
self.timestamp = timestamp
self.displacement = displacement
}

func execute(on target: EventModifiable) {
// TODO: Use PositionSystem from target to modify entity of entityId
}
}
Expand Up @@ -41,4 +41,16 @@ class EntityManager {
}
self.entityMap[entity.id] = entity
}

func component<T: Component>(ofType type: T.Type, of entityId: EntityId) -> T? {
guard let components = entityComponentMap[entityId] else {
return nil
}

guard let componentId = components.first(where: { componentMap[$0] is T }) else {
return nil
}

return componentMap[componentId] as? T
}
}
37 changes: 37 additions & 0 deletions star-dash/star-dash/GameEngine/GameEngine.swift
@@ -0,0 +1,37 @@
//
// GameEngine.swift
// star-dash
//
// Created by Ho Jun Hao on 14/3/24.
//

import Foundation

class GameEngine {
private let systemManager: SystemManager
private let entityManager: EntityManager
private let eventManager: EventManager

init(scene: GameScene) {
self.systemManager = SystemManager()
self.entityManager = EntityManager()
self.eventManager = EventManager()
// TODO: link game engine to renderer

setUpSystems()
}

func update(by deltaTime: TimeInterval) {
systemManager.update(by: deltaTime)
eventManager.executeAll(on: self)
}

private func setUpSystems() {
systemManager.add(PositionSystem(entityManager, dispatcher: self))
systemManager.add(PhysicsSystem(entityManager, dispatcher: self))
}
}

extension GameEngine: EventModifiable {
// TODO: functions of event modifiable
}
69 changes: 69 additions & 0 deletions star-dash/star-dash/GameEngine/Systems/PhysicsSystem.swift
@@ -0,0 +1,69 @@
//
// PhysicsSystem.swift
// star-dash
//
// Created by Ho Jun Hao on 13/3/24.
//

import Foundation

class PhysicsSystem: System {
var isActive: Bool
var dispatcher: EventModifiable?
var entityManager: EntityManager

init(_ entityManager: EntityManager, dispatcher: EventModifiable? = nil) {
self.isActive = true
self.dispatcher = dispatcher
self.entityManager = entityManager
}

func update(by deltaTime: TimeInterval) {
let physicsComponents = entityManager.componentMap.values.compactMap({ $0 as? PhysicsComponent })

for physicsComponent in physicsComponents {
physicsComponent.force = .zero
}
}

func isMoving(_ entityId: EntityId) -> Bool {
guard let physicsComponent = getPhysicsComponent(of: entityId) else {
return false
}

return physicsComponent.velocity != .zero
}

func isJumping(_ entityId: EntityId) -> Bool {
guard let physicsComponent = getPhysicsComponent(of: entityId) else {
return false
}

return physicsComponent.velocity.dy != .zero
}

func applyForce(to entityId: EntityId, newForce: CGVector) {
guard let physicsComponent = getPhysicsComponent(of: entityId) else {
return
}

let newForce = CGVector(dx: physicsComponent.force.dx + newForce.dx,
dy: physicsComponent.force.dy + newForce.dy)

physicsComponent.force = newForce
}

func sync(entityVelocityMap: [EntityId: CGVector]) {
for (entityId, newVelocity) in entityVelocityMap {
guard let physicsComponent = getPhysicsComponent(of: entityId) else {
continue
}

physicsComponent.velocity = newVelocity
}
}

private func getPhysicsComponent(of entityId: EntityId) -> PhysicsComponent? {
entityManager.component(ofType: PhysicsComponent.self, of: entityId)
}
}
50 changes: 50 additions & 0 deletions star-dash/star-dash/GameEngine/Systems/PositionSystem.swift
@@ -0,0 +1,50 @@
//
// PositionSystem.swift
// star-dash
//
// Created by Ho Jun Hao on 13/3/24.
//

import Foundation

class PositionSystem: System {
var isActive: Bool
var dispatcher: EventModifiable?
var entityManager: EntityManager

init(_ entityManager: EntityManager, dispatcher: EventModifiable? = nil) {
self.isActive = true
self.entityManager = entityManager
self.dispatcher = dispatcher
}

func move(entityId: EntityId, to newPosition: CGPoint) {
guard let positionComponent = getPositionComponent(of: entityId) else {
return
}

positionComponent.setPosition(position: newPosition)
}

func rotate(entityId: EntityId, to newRotation: Float) {
guard let positionComponent = getPositionComponent(of: entityId) else {
return
}

positionComponent.setRotation(rotation: newRotation)
}

func sync(entityPositionMap: [EntityId: CGPoint], entityRotationMap: [EntityId: Float]) {
for (entityId, newPosition) in entityPositionMap {
move(entityId: entityId, to: newPosition)
}

for (entityId, newRotation) in entityRotationMap {
rotate(entityId: entityId, to: newRotation)
}
}

private func getPositionComponent(of entityId: EntityId) -> PositionComponent? {
entityManager.component(ofType: PositionComponent.self, of: entityId)
}
}
20 changes: 20 additions & 0 deletions star-dash/star-dash/GameEngine/Systems/System.swift
@@ -0,0 +1,20 @@
//
// System.swift
// star-dash
//
// Created by Ho Jun Hao on 13/3/24.
//

import Foundation

protocol System {
var isActive: Bool { get set }
var dispatcher: EventModifiable? { get set }
var entityManager: EntityManager { get set }

func update(by deltaTime: TimeInterval)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I clarify what is the role of this method? I'm a bit confused because I noticed that not all systems (like PositionSystem) override this method. And in PhysicsSystem, the update method basically sets all the force vectors to 0. Under what circumstance is this method called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It more like for timed components in the game, e.g. those that after a certain time will stop working like powerups

}

extension System {
func update(by deltaTime: TimeInterval) {}
}
22 changes: 22 additions & 0 deletions star-dash/star-dash/GameEngine/Systems/SystemManager.swift
@@ -0,0 +1,22 @@
//
// SystemManager.swift
// star-dash
//
// Created by Ho Jun Hao on 13/3/24.
//

import Foundation

class SystemManager {
private var systems: [System] = []

func add(_ system: System) {
systems.append(system)
}

func update(by deltaTime: TimeInterval) {
for system in systems where system.isActive {
system.update(by: deltaTime)
}
}
}