Skip to content

Commit

Permalink
Part 15
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklockwood committed Mar 9, 2020
1 parent b8784a4 commit 04ab7ac
Show file tree
Hide file tree
Showing 16 changed files with 479 additions and 54 deletions.
9 changes: 7 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,7 +71,11 @@ 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
Expand Down
88 changes: 71 additions & 17 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,27 +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 @@ -82,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 @@ -122,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 []
}
}
2 changes: 1 addition & 1 deletion Source/Engine/Rect.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//

public struct Rect {
var min, max: Vector
public var min, max: Vector

public init(min: Vector, max: Vector) {
self.min = min
Expand Down
22 changes: 2 additions & 20 deletions Source/Engine/Textures.swift → Source/Engine/Texture.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//
// Textures.swift
// Texture.swift
// Engine
//
// Created by Nick Lockwood on 05/06/2019.
// Created by Nick Lockwood on 13/02/2020.
// Copyright © 2019 Nick Lockwood. All rights reserved.
//

Expand All @@ -29,21 +29,3 @@ public enum Texture: String, CaseIterable {
case elevatorFloor, elevatorCeiling, elevatorSideWall, elevatorBackWall
case medkit
}

public struct Textures {
private let textures: [Texture: Bitmap]
}

public extension Textures {
init(loader: (String) -> Bitmap) {
var textures = [Texture: Bitmap]()
for texture in Texture.allCases {
textures[texture] = loader(texture.rawValue)
}
self.init(textures: textures)
}

subscript(_ texture: Texture) -> Bitmap {
return textures[texture]!
}
}
2 changes: 1 addition & 1 deletion Source/Engine/Vector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// Copyright © 2019 Nick Lockwood. All rights reserved.
//

public struct Vector: Equatable {
public struct Vector: Hashable {
public var x, y: Double

public init(x: Double, y: Double) {
Expand Down
64 changes: 64 additions & 0 deletions Source/Engine/World.swift
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,21 @@ public extension World {
return map.things[y * map.width + x] == .door
}

func door(at x: Int, _ y: Int) -> Door? {
guard isDoor(at: x, y) else {
return nil
}
return doors.first(where: {
Int($0.position.x) == x && Int($0.position.y) == y
})
}

func pushwall(at x: Int, _ y: Int) -> Pushwall? {
return pushwalls.first(where: {
Int($0.position.x) == x && Int($0.position.y) == y
})
}

func `switch`(at x: Int, _ y: Int) -> Switch? {
guard map.things[y * map.width + x] == .switch else {
return nil
Expand All @@ -342,3 +357,52 @@ public extension World {
})
}
}

extension World: Graph {
public struct Node: Hashable {
public let x, y: Double

public init(x: Double, y: Double) {
self.x = x.rounded(.down) + 0.5
self.y = y.rounded(.down) + 0.5
}
}

public func findPath(
from start: Vector,
to end: Vector,
maxDistance: Double = 50
) -> [Vector] {
return findPath(
from: Node(x: start.x, y: start.y),
to: Node(x: end.x, y: end.y),
maxDistance: maxDistance
).map { node in
Vector(x: node.x, y: node.y)
}
}

public func nodesConnectedTo(_ node: Node) -> [Node] {
return [
Node(x: node.x - 1, y: node.y),
Node(x: node.x + 1, y: node.y),
Node(x: node.x, y: node.y - 1),
Node(x: node.x, y: node.y + 1),
].filter { node in
let x = Int(node.x), y = Int(node.y)
return map[x, y].isWall == false && pushwall(at: x, y) == nil
}
}

public func estimatedDistance(from a: Node, to b: Node) -> Double {
return abs(b.x - a.x) + abs(b.y - a.y)
}

public func stepDistance(from a: Node, to b: Node) -> Double {
let x = Int(b.x), y = Int(b.y)
if door(at: x, y)?.state == .closed {
return 5
}
return 1
}
}
Loading

0 comments on commit 04ab7ac

Please sign in to comment.