Skip to content

Commit

Permalink
Part 5
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklockwood committed Jul 17, 2019
1 parent 265ad75 commit 8fad3f3
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 5 deletions.
63 changes: 63 additions & 0 deletions Source/Engine/Billboard.swift
@@ -0,0 +1,63 @@
//
// Billboard.swift
// Engine
//
// Created by Nick Lockwood on 05/06/2019.
// Copyright © 2019 Nick Lockwood. All rights reserved.
//

public struct Billboard {
public var start: Vector
public var direction: Vector
public var length: Double

public init(start: Vector, direction: Vector, length: Double) {
self.start = start
self.direction = direction
self.length = length
}
}

public extension Billboard {
var end: Vector {
return start + direction * length
}

func hitTest(_ ray: Ray) -> Vector? {
var lhs = ray, rhs = Ray(origin: start, direction: direction)

// Ensure rays are never exactly vertical
let epsilon = 0.00001
if abs(lhs.direction.x) < epsilon {
lhs.direction.x = epsilon
}
if abs(rhs.direction.x) < epsilon {
rhs.direction.x = epsilon
}

// Calculate slopes and intercepts
let (slope1, intercept1) = lhs.slopeIntercept
let (slope2, intercept2) = rhs.slopeIntercept

// Check if slopes are parallel
if slope1 == slope2 {
return nil
}

// Find intersection point
let x = (intercept1 - intercept2) / (slope2 - slope1)
let y = slope1 * x + intercept1

// Check intersection point is in range
let distanceAlongRay = (x - lhs.origin.x) / lhs.direction.x
if distanceAlongRay < 0 {
return nil
}
let distanceAlongBillboard = (x - rhs.origin.x) / rhs.direction.x
if distanceAlongBillboard < 0 || distanceAlongBillboard > length {
return nil
}

return Vector(x: x, y: y)
}
}
12 changes: 11 additions & 1 deletion Source/Engine/Bitmap.swift
Expand Up @@ -72,7 +72,17 @@ public extension Bitmap {
for y in max(0, start) ..< min(self.height, end) {
let sourceY = (Double(y) - point.y) * stepY
let sourceColor = source[sourceX, Int(sourceY)]
self[Int(point.x), y] = sourceColor
blendPixel(at: Int(point.x), y, with: sourceColor)
}
}

mutating func blendPixel(at x: Int, _ y: Int, with newColor: Color) {
let oldColor = self[x, y]
let inverseAlpha = 1 - Double(newColor.a) / 255
self[x, y] = Color(
r: UInt8(Double(oldColor.r) * inverseAlpha) + newColor.r,
g: UInt8(Double(oldColor.g) * inverseAlpha) + newColor.g,
b: UInt8(Double(oldColor.b) * inverseAlpha) + newColor.b
)
}
}
15 changes: 15 additions & 0 deletions Source/Engine/Monster.swift
@@ -0,0 +1,15 @@
//
// Monster.swift
// Engine
//
// Created by Nick Lockwood on 02/06/2019.
// Copyright © 2019 Nick Lockwood. All rights reserved.
//

public struct Monster {
public var position: Vector

public init(position: Vector) {
self.position = position
}
}
8 changes: 8 additions & 0 deletions Source/Engine/Ray.swift
Expand Up @@ -14,3 +14,11 @@ public struct Ray {
self.direction = direction
}
}

public extension Ray {
var slopeIntercept: (slope: Double, intercept: Double) {
let slope = direction.y / direction.x
let intercept = origin.y - slope * origin.x
return (slope, intercept)
}
}
28 changes: 28 additions & 0 deletions Source/Engine/Renderer.swift
Expand Up @@ -24,6 +24,16 @@ public extension Renderer {
let viewCenter = world.player.position + world.player.direction * focalLength
let viewStart = viewCenter - viewPlane / 2

// Sort sprites by distance
var spritesByDistance: [(distance: Double, sprite: Billboard)] = []
for sprite in world.sprites {
let spriteDistance = (sprite.start - world.player.position).length
spritesByDistance.append(
(distance: spriteDistance, sprite: sprite)
)
}
spritesByDistance.sort(by: { $0.distance > $1.distance })

// Cast rays
let columns = bitmap.width
let step = viewPlane / Double(columns)
Expand Down Expand Up @@ -78,6 +88,24 @@ public extension Renderer {
bitmap[x, bitmap.height - y] = ceilingTexture[normalized: textureX, textureY]
}

// Draw sprites
for (_, sprite) in spritesByDistance {
guard let hit = sprite.hitTest(ray) else {
continue
}
let spriteDistance = (hit - ray.origin).length
if spriteDistance > wallDistance {
continue
}
let perpendicular = spriteDistance / distanceRatio
let height = wallHeight / perpendicular * Double(bitmap.height)
let spriteX = (hit - sprite.start).length / sprite.length
let textureX = Int(spriteX * Double(wallTexture.width))
let spriteTexture = textures[.monster]
let start = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 + 0.001)
bitmap.drawColumn(textureX, of: spriteTexture, at: start, height: height)
}

columnPosition += step
}
}
Expand Down
1 change: 1 addition & 0 deletions Source/Engine/Textures.swift
Expand Up @@ -13,6 +13,7 @@ public enum Texture: String, CaseIterable {
case floor
case crackFloor
case ceiling
case monster
}

public struct Textures {
Expand Down
1 change: 1 addition & 0 deletions Source/Engine/Thing.swift
Expand Up @@ -9,4 +9,5 @@
public enum Thing: Int, Decodable {
case nothing
case player
case monster
}
15 changes: 15 additions & 0 deletions Source/Engine/World.swift
Expand Up @@ -8,10 +8,12 @@

public struct World {
public let map: Tilemap
public var monsters: [Monster]
public var player: Player!

public init(map: Tilemap) {
self.map = map
self.monsters = []
for y in 0 ..< map.height {
for x in 0 ..< map.width {
let position = Vector(x: Double(x) + 0.5, y: Double(y) + 0.5)
Expand All @@ -21,6 +23,8 @@ public struct World {
break
case .player:
self.player = Player(position: position)
case .monster:
monsters.append(Monster(position: position))
}
}
}
Expand All @@ -40,4 +44,15 @@ public extension World {
player.position -= intersection
}
}

var sprites: [Billboard] {
let spritePlane = player.direction.orthogonal
return monsters.map { monster in
Billboard(
start: monster.position - spritePlane / 2,
direction: spritePlane,
length: 1
)
}
}
}
8 changes: 8 additions & 0 deletions Source/Rampage.xcodeproj/project.pbxproj
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
012A0C4D22C96E150068E8EF /* Tile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C4C22C96E150068E8EF /* Tile.swift */; };
012A0C4F22C96E1F0068E8EF /* Thing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C4E22C96E1F0068E8EF /* Thing.swift */; };
012A0C6222CC200E0068E8EF /* Billboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C6022CC200D0068E8EF /* Billboard.swift */; };
012A0C6322CC200E0068E8EF /* Monster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C6122CC200D0068E8EF /* Monster.swift */; };
016E41B3228E9A5B00ACF137 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016E41B2228E9A5B00ACF137 /* AppDelegate.swift */; };
016E41B5228E9A5B00ACF137 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016E41B4228E9A5B00ACF137 /* ViewController.swift */; };
016E41B8228E9A5B00ACF137 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 016E41B6228E9A5B00ACF137 /* Main.storyboard */; };
Expand Down Expand Up @@ -59,6 +61,8 @@
/* Begin PBXFileReference section */
012A0C4C22C96E150068E8EF /* Tile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tile.swift; sourceTree = "<group>"; };
012A0C4E22C96E1F0068E8EF /* Thing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thing.swift; sourceTree = "<group>"; };
012A0C6022CC200D0068E8EF /* Billboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Billboard.swift; sourceTree = "<group>"; };
012A0C6122CC200D0068E8EF /* Monster.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Monster.swift; sourceTree = "<group>"; };
016E41AF228E9A5B00ACF137 /* Rampage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Rampage.app; sourceTree = BUILT_PRODUCTS_DIR; };
016E41B2228E9A5B00ACF137 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
016E41B4228E9A5B00ACF137 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -139,9 +143,11 @@
016E41CA228E9A8600ACF137 /* Engine */ = {
isa = PBXGroup;
children = (
012A0C6022CC200D0068E8EF /* Billboard.swift */,
01D09AF222A482030052745A /* Bitmap.swift */,
01D09AF022A481AB0052745A /* Color.swift */,
01D09B0222A4958E0052745A /* Input.swift */,
012A0C6122CC200D0068E8EF /* Monster.swift */,
01D09AFA22A485040052745A /* Player.swift */,
01D09B0422A5C9DB0052745A /* Ray.swift */,
01D09AFC22A4873B0052745A /* Rect.swift */,
Expand Down Expand Up @@ -282,6 +288,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
012A0C6322CC200E0068E8EF /* Monster.swift in Sources */,
01D09B0B22A7F7570052745A /* Textures.swift in Sources */,
01D09AFF22A48E990052745A /* Tilemap.swift in Sources */,
01D09AFD22A4873B0052745A /* Rect.swift in Sources */,
Expand All @@ -291,6 +298,7 @@
01D09AF322A482030052745A /* Bitmap.swift in Sources */,
01D09B0722A6E09B0052745A /* Rotation.swift in Sources */,
01D09AFB22A485040052745A /* Player.swift in Sources */,
012A0C6222CC200E0068E8EF /* Billboard.swift in Sources */,
01ADC64022B9846B00DC8AAD /* World.swift in Sources */,
012A0C4D22C96E150068E8EF /* Tile.swift in Sources */,
01D09B0322A4958E0052745A /* Input.swift in Sources */,
Expand Down
80 changes: 80 additions & 0 deletions Source/Rampage.xcodeproj/xcshareddata/xcschemes/Engine.xcscheme
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1010"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "016E41C8228E9A8600ACF137"
BuildableName = "Engine.framework"
BlueprintName = "Engine"
ReferencedContainer = "container:Rampage.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "016E41C8228E9A8600ACF137"
BuildableName = "Engine.framework"
BlueprintName = "Engine"
ReferencedContainer = "container:Rampage.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "016E41C8228E9A8600ACF137"
BuildableName = "Engine.framework"
BlueprintName = "Engine"
ReferencedContainer = "container:Rampage.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
21 changes: 21 additions & 0 deletions Source/Rampage/Assets.xcassets/monster.imageset/Contents.json
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "monster.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions Source/Rampage/Map.json
Expand Up @@ -12,12 +12,12 @@
],
"things": [
0, 0, 0, 0, 0, 0, 0, 0,
0, 2, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 2, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 0, 0,
0, 0, 2, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 0, 2, 0,
0, 0, 0, 0, 0, 0, 0, 0
]
}

0 comments on commit 8fad3f3

Please sign in to comment.