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

Multithreaded rendering #8

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Source/Engine/Actor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public extension Actor {
return rect.intersection(with: door.rect)
}

func intersection(with pushwall: Pushwall) -> Vector? {
return rect.intersection(with: pushwall.rect)
}

func intersection(with world: World) -> Vector? {
if let intersection = intersection(with: world.map) {
return intersection
Expand All @@ -50,6 +54,11 @@ public extension Actor {
return intersection
}
}
for pushwall in world.pushwalls where pushwall.position != position {
if let intersection = intersection(with: pushwall) {
return intersection
}
}
return nil
}

Expand All @@ -67,4 +76,20 @@ public extension Actor {
attempts -= 1
}
}

func isStuck(in world: World) -> Bool {
// If outside map
if position.x < 1 || position.x > world.map.size.x - 1 ||
position.y < 1 || position.y > world.map.size.y - 1 {
return true
}
// If stuck in a wall
if world.map[Int(position.x), Int(position.y)].isWall {
return true
}
// If stuck in pushwall
return world.pushwalls.contains(where: {
abs(position.x - $0.position.x) < 0.6 && abs(position.y - $0.position.y) < 0.6
})
}
}
7 changes: 3 additions & 4 deletions Source/Engine/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ public extension Color {
static let clear = Color(r: 0, g: 0, b: 0, a: 0)
static let black = Color(r: 0, g: 0, b: 0)
static let white = Color(r: 255, g: 255, b: 255)
static let gray = Color(r: 192, g: 192, b: 192)
static let red = Color(r: 255, g: 0, b: 0)
static let green = Color(r: 0, g: 255, b: 0)
static let blue = Color(r: 0, g: 0, b: 255)
static let red = Color(r: 217, g: 87, b: 99)
static let green = Color(r: 153, g: 229, b: 80)
static let yellow = Color(r: 251, g: 242, b: 54)
}
11 changes: 9 additions & 2 deletions Source/Engine/Door.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public struct Door {
public extension Door {
var rect: Rect {
let position = self.position + direction * (offset - 0.5)
return Rect(min: position, max: position + direction)
let depth = direction.orthogonal * 0.1
return Rect(min: position + depth, max: position + direction - depth)
}

var offset: Double {
Expand Down Expand Up @@ -70,8 +71,13 @@ public extension Door {
mutating func update(in world: inout World) {
switch state {
case .closed:
if world.player.intersection(with: self) != nil {
if world.player.intersection(with: self) != nil ||
world.monsters.contains(where: { monster in
monster.isDead == false &&
monster.intersection(with: self) != nil
}) {
state = .opening
world.playSound(.doorSlide, at: position)
time = 0
}
case .opening:
Expand All @@ -82,6 +88,7 @@ public extension Door {
case .open:
if time >= closeDelay {
state = .closing
world.playSound(.doorSlide, at: position)
time = 0
}
case .closing:
Expand Down
94 changes: 75 additions & 19 deletions Source/Engine/Monster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
public enum MonsterState {
case idle
case chasing
case blocked
case scratching
case hurt
case dead
Expand All @@ -24,6 +25,7 @@ public struct Monster: Actor {
public var animation: Animation = .monsterIdle
public let attackCooldown: Double = 0.4
public private(set) var lastAttackTime: Double = 0
public private(set) var path: [Vector] = []

public init(position: Vector) {
self.position = position
Expand All @@ -38,26 +40,42 @@ public extension Monster {
mutating func update(in world: inout World) {
switch state {
case .idle:
if canSeePlayer(in: world) {
if canSeePlayer(in: world) || canHearPlayer(in: world) {
state = .chasing
animation = .monsterWalk
world.playSound(.monsterGroan, at: position)
}
case .chasing:
guard canSeePlayer(in: world) else {
state = .idle
animation = .monsterIdle
velocity = Vector(x: 0, y: 0)
if canSeePlayer(in: world) || canHearPlayer(in: world) {
path = world.findPath(from: position, to: world.player.position)
if canReachPlayer(in: world) {
state = .scratching
animation = .monsterScratch
lastAttackTime = -attackCooldown
velocity = Vector(x: 0, y: 0)
break
}
}
guard let destination = path.first else {
break
}
if canReachPlayer(in: world) {
state = .scratching
animation = .monsterScratch
lastAttackTime = -attackCooldown
velocity = Vector(x: 0, y: 0)
let direction = destination - position
let distance = direction.length
if distance < 0.1 {
path.removeFirst()
break
}
let direction = world.player.position - position
velocity = direction * (speed / direction.length)
velocity = direction * (speed / distance)
if world.monsters.contains(where: isBlocked(by:)) {
state = .blocked
animation = .monsterBlocked
velocity = Vector(x: 0, y: 0)
}
case .blocked:
if animation.isCompleted {
state = .chasing
animation = .monsterWalk
}
case .scratching:
guard canReachPlayer(in: world) else {
state = .chasing
Expand All @@ -67,11 +85,12 @@ public extension Monster {
if animation.time - lastAttackTime >= attackCooldown {
lastAttackTime = animation.time
world.hurtPlayer(10)
world.playSound(.monsterSwipe, at: position)
}
case .hurt:
if animation.isCompleted {
state = .idle
animation = .monsterIdle
state = .chasing
animation = .monsterWalk
}
case .dead:
if animation.isCompleted {
Expand All @@ -80,13 +99,47 @@ public extension Monster {
}
}

func isBlocked(by other: Monster) -> Bool {
// Ignore dead or inactive monsters
if other.isDead || other.state != .chasing {
return false
}
// Ignore if too far away
let direction = other.position - position
let distance = direction.length
if distance > radius + other.radius + 0.5 {
return false
}
// Is standing in the direction we're moving
return (direction / distance).dot(velocity / velocity.length) > 0.5
}

func canSeePlayer(in world: World) -> Bool {
let direction = world.player.position - position
var direction = world.player.position - position
let playerDistance = direction.length
let ray = Ray(origin: position, direction: direction / playerDistance)
let wallHit = world.hitTest(ray)
let wallDistance = (wallHit - position).length
return wallDistance > playerDistance
direction /= playerDistance
let orthogonal = direction.orthogonal
for offset in [-0.2, 0.2] {
let origin = position + orthogonal * offset
let ray = Ray(origin: origin, direction: direction)
let wallHit = world.hitTest(ray)
let wallDistance = (wallHit - position).length
if wallDistance > playerDistance {
return true
}
}
return false
}

func canHearPlayer(in world: World) -> Bool {
guard world.player.state == .firing else {
return false
}
return world.findPath(
from: position,
to: world.player.position,
maxDistance: 12
).isEmpty == false
}

func canReachPlayer(in world: World) -> Bool {
Expand Down Expand Up @@ -120,6 +173,9 @@ public extension Animation {
static let monsterIdle = Animation(frames: [
.monster
], duration: 0)
static let monsterBlocked = Animation(frames: [
.monster
], duration: 1)
static let monsterWalk = Animation(frames: [
.monsterWalk1,
.monster,
Expand Down
87 changes: 87 additions & 0 deletions Source/Engine/Pathfinder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// Pathfinder.swift
// Engine
//
// Created by Nick Lockwood on 10/02/2020.
// Copyright © 2020 Nick Lockwood. All rights reserved.
//

import Foundation

public protocol Graph {
associatedtype Node: Hashable

func nodesConnectedTo(_ node: Node) -> [Node]
func estimatedDistance(from a: Node, to b: Node) -> Double
func stepDistance(from a: Node, to b: Node) -> Double
}

private class Path<Node> {
let head: Node
let tail: Path?
let distanceTravelled: Double
let totalDistance: Double

init(head: Node, tail: Path?, stepDistance: Double, remaining: Double) {
self.head = head
self.tail = tail
self.distanceTravelled = (tail?.distanceTravelled ?? 0) + stepDistance
self.totalDistance = distanceTravelled + remaining
}

var nodes: [Node] {
var nodes = [head]
var tail = self.tail
while let path = tail {
nodes.insert(path.head, at: 0)
tail = path.tail
}
nodes.removeFirst()
return nodes
}
}

public extension Graph {
func findPath(from start: Node, to end: Node, maxDistance: Double) -> [Node] {
var visited = Set([start])
var paths = [Path(
head: start,
tail: nil,
stepDistance: 0,
remaining: estimatedDistance(from: start, to: end)
)]

while let path = paths.popLast() {
// Finish if goal reached
if path.head == end {
return path.nodes
}

// Get connected nodes
for node in nodesConnectedTo(path.head) where !visited.contains(node) {
visited.insert(node)
let next = Path(
head: node,
tail: path,
stepDistance: stepDistance(from: path.head, to: node),
remaining: estimatedDistance(from: node, to: end)
)
// Skip this node if max distance exceeded
if next.totalDistance > maxDistance {
break
}
// Insert shortest path last
if let index = paths.firstIndex(where: {
$0.totalDistance <= next.totalDistance
}) {
paths.insert(next, at: index)
} else {
paths.append(next)
}
}
}

// Unreachable
return []
}
}
46 changes: 46 additions & 0 deletions Source/Engine/Pickup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Pickup.swift
// Engine
//
// Created by Nick Lockwood on 27/01/2020.
// Copyright © 2020 Nick Lockwood. All rights reserved.
//

public enum PickupType {
case medkit
case shotgun
}

public struct Pickup: Actor {
public let type: PickupType
public var radius: Double = 0.5
public var position: Vector

public init(type: PickupType, position: Vector) {
self.type = type
self.position = position
}
}

public extension Pickup {
var isDead: Bool { return false }

var texture: Texture {
switch type {
case .medkit:
return .medkit
case .shotgun:
return .shotgunPickup
}
}

func billboard(for ray: Ray) -> Billboard {
let plane = ray.direction.orthogonal
return Billboard(
start: position - plane / 2,
direction: plane,
length: 1,
texture: texture
)
}
}
Loading