Skip to content

Instantly share code, notes, and snippets.

@brookinc
Last active December 29, 2018 05:42
Show Gist options
  • Save brookinc/2767ca8ca1963622a94f984e9267d8bf to your computer and use it in GitHub Desktop.
Save brookinc/2767ca8ca1963622a94f984e9267d8bf to your computer and use it in GitHub Desktop.
A simple Entity-Component System experiment in Swift.
import Foundation
#if swift(>=4.2)
// no shim necessary
#else
// add shims for swift-4.2-style random(in:) methods
// NOTE: unlike Swift 4.2's implementations, these versions are prone to modulo bias,
// are not cryptographically secure, and don't support the full range of Int64 values
#if os(Linux)
srandom(UInt32(time(nil)))
func randomSystemInt() -> Int {
return SwiftGlibc.random()
}
#elseif os(macOS)
func randomSystemInt() -> Int {
return Int(arc4random())
}
#endif
extension Int {
static func random(in range: Range<Int>) -> Int {
return Int(randomSystemInt() % (range.upperBound - range.lowerBound)) + range.lowerBound
}
static func random(in range: ClosedRange<Int>) -> Int {
return Int(randomSystemInt() % (range.upperBound - range.lowerBound + 1)) + range.lowerBound
}
}
extension Double {
static func random(in range: Range<Double>) -> Double {
return (Double(randomSystemInt()) / Double(Int32.max) * (range.upperBound - range.lowerBound)) + range.lowerBound
}
static func random(in range: ClosedRange<Double>) -> Double {
return (Double(randomSystemInt()) / Double(Int32.max - 1) * (range.upperBound - range.lowerBound)) + range.lowerBound
}
}
extension Bool {
static func random() -> Bool {
return (Int.random(in: 0 ... 1) == 0)
}
}
#endif
// system configuration variables
let numEntities = 5
let entityMaxID = 100
let coordinateRange = 10
let numTicks = 8
let componentProbability = 0.25
// component configuration variables
let healthRange = 30.0 ... 100.0
let healthDecay = 10.0
let maxAttackStrength = 10.0
/// The core class for entities.
class Entity {
/// The entity's ID value.
var id: Int
/// The entity's array of components
var components: [Component] = []
init(id newId: Int) {
id = newId
}
func print() {
Swift.print("\(id)", terminator: "")
for component in components {
component.print()
}
Swift.print("")
}
func tick() {
for component in components {
component.tick()
}
}
func hasComponent<C: Component>(_ component: C.Type) -> Bool {
return getComponent(component.self) != nil
}
func getComponent<C: Component>(_: C.Type) -> C? {
for component in components {
if let validComponent = component as? C {
return validComponent
}
}
return nil
}
}
/// The base class for components.
class Component {
/// TODO: can swiftlint detect this strong reference cycle?
//let entity: Entity
/// The entity to which this component is attached (`unowned` to prevent a strong reference cycle).
unowned let entity: Entity
init(_ owner: Entity) {
entity = owner
}
func tick() {
}
func print() {
}
}
/// A component for entities with positions.
class PositionComponent: Component {
var pos = (x: 0, y: 0) {
didSet {
pos.x = min(max(pos.x, 0), coordinateRange)
pos.y = min(max(pos.y, 0), coordinateRange)
}
}
override func print() {
Swift.print(" [\(pos.x), \(pos.y)]", terminator: "")
}
func distSqFrom(pos: (Int, Int)) -> Int {
return (self.pos.x - pos.0) * (self.pos.x - pos.0) + (self.pos.y - pos.1) * (self.pos.y - pos.1)
}
}
/// A component to subject entities to gravity.
class GravityComponent: Component {
override func tick() {
entity.getComponent(PositionComponent.self)?.pos.y -= 1
}
override func print() {
Swift.print(" g", terminator: "")
}
}
/// A component to make entities locomote randomly.
class RandomPropulsionComponent: Component {
override func tick() {
if Bool.random() {
entity.getComponent(PositionComponent.self)?.pos.x += Int.random(in: -1 ... 1)
} else {
entity.getComponent(PositionComponent.self)?.pos.y += Int.random(in: -1 ... 1)
}
}
override func print() {
Swift.print(" rp", terminator: "")
}
}
/// A component for entities that can be hurt/be healed/die.
class HealthComponent: Component {
let maxHealth: Double
private(set) var health: Double {
didSet {
health = min(max(health, 0.0), Double(maxHealth))
}
}
init(_ owner: Entity, max: Double) {
maxHealth = max
health = max
super.init(owner)
}
override func tick() {
health -= healthDecay
/// TODO: when health is <= 0.0, die / deallocate?
}
override func print() {
Swift.print(" h=\(health)/\(maxHealth)", terminator: "")
}
func hurt(_ damage: Double) {
if damage > 0.0 {
health -= damage
}
}
func heal(_ restore: Double) {
if restore > 0.0 {
health += restore
}
}
}
/// A component that allows an entity to attack nearby entities.
class AttackNearbyComponent: Component {
private(set) var attackStrength: Double
init(_ owner: Entity, strength: Double) {
attackStrength = strength
super.init(owner)
}
override func tick() {
guard entity.hasComponent(PositionComponent.self) else {
return
}
/// TODO: find closest entity with a HealthComponent
/// TODO: determine attack success probability by distance
/// TODO: determine max attack damage by distance
}
override func print() {
Swift.print(" a=\(attackStrength)", terminator: "")
}
}
// spawn some entities at random positions
print("\nEntities:")
var entities: [Entity] = []
for _ in 0 ..< numEntities {
let entity = Entity(id: Int.random(in: 1 ... entityMaxID))
let posComponent = PositionComponent(entity)
posComponent.pos.x = Int.random(in: 0 ..< coordinateRange)
posComponent.pos.y = Int.random(in: 0 ..< coordinateRange)
entity.components.append(posComponent)
entity.print()
entities.append(entity)
}
// assign components to them randomly
print("\nComponents:")
for entity in entities {
if Double.random(in: 0.0 ... 1.0) < componentProbability {
entity.components.append(GravityComponent(entity))
}
if Double.random(in: 0.0 ... 1.0) < componentProbability {
entity.components.append(RandomPropulsionComponent(entity))
}
if Double.random(in: 0.0 ... 1.0) < componentProbability {
entity.components.append(HealthComponent(entity, max: Double.random(in: healthRange).rounded()))
}
if Double.random(in: 0.0 ... 1.0) < componentProbability {
entity.components.append(AttackNearbyComponent(entity, strength: Double.random(in: 0.0 ... maxAttackStrength).rounded()))
}
}
for entity in entities {
if entity.hasComponent(GravityComponent.self) {
print("Entity \(entity.id) has a GravityComponent")
}
if entity.hasComponent(RandomPropulsionComponent.self) {
print("Entity \(entity.id) has a RandomPropulsionComponent")
}
if entity.hasComponent(HealthComponent.self) {
print("Entity \(entity.id) has a HealthComponent")
}
if entity.hasComponent(AttackNearbyComponent.self) {
print("Entity \(entity.id) has a AttackNearbyComponent")
}
}
// simulate their interactions
print("\nInitial:")
for entity in entities {
entity.print()
}
for tickNum in 0 ..< numTicks {
for entity in entities {
entity.tick()
}
print("\nAfter Frame \(tickNum):")
for entity in entities {
entity.print()
}
}
/// TODO: parse entities and components from JSON
/// TODO: serialize / deserialize (JSON? binary?)
/// TODO: move health depletion into a PoisonComponent
/// TODO: convert entities from Array to Dictionary (with ID as index)
# force linefeed (Unix-style) line endings for all .swift files, to prevent SwiftLint
# vertical_whitespace warnings (and line number mismatches) on Windows host machines
*.swift eol=lf
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment