Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: nicklockwood/RetroRampage
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 86023291c1392d08a915d957d9715591bb9f8e20
Choose a base ref
..
head repository: nicklockwood/RetroRampage
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 386b31954e51f6e3bcf306ab13df272ef5e235ce
Choose a head ref
Showing with 1,184 additions and 63 deletions.
  1. +7 −0 .travis.yml
  2. +26 −16 README.md
  3. +42 −27 Source/Engine/Bitmap.swift
  4. +4 −0 Source/Engine/Color.swift
  5. +2 −2 Source/Engine/Renderer.swift
  6. +124 −0 Source/Rampage.xcodeproj/project.pbxproj
  7. +22 −0 ...eddata/xcbaselines/01D0F5D522F80E1600682CA1.xcbaseline/551B7382-0062-43E9-B480-E9015647D149.plist
  8. +40 −0 Source/Rampage.xcodeproj/xcshareddata/xcbaselines/01D0F5D522F80E1600682CA1.xcbaseline/Info.plist
  9. +11 −1 Source/Rampage.xcodeproj/xcshareddata/xcschemes/Rampage.xcscheme
  10. +12 −10 Source/Rampage/UIImage+Bitmap.swift
  11. +7 −2 Source/Rampage/ViewController.swift
  12. +22 −0 Source/RampageTests/Info.plist
  13. +23 −0 Source/RampageTests/RampageTests.swift
  14. BIN Tutorial/Images/AddUnitTestingBundle.png
  15. BIN Tutorial/Images/Architecture.png
  16. BIN Tutorial/Images/AttackAnimation.png
  17. BIN Tutorial/Images/BigPixelFizzle.png
  18. BIN Tutorial/Images/BitmapOpacityTest.png
  19. BIN Tutorial/Images/BlackSpriteBackground.png
  20. BIN Tutorial/Images/BlendColorCrash.png
  21. BIN Tutorial/Images/BlurryBluePixel.png
  22. BIN Tutorial/Images/ClearSpriteBackground.png
  23. BIN Tutorial/Images/CollisionLoop.png
  24. BIN Tutorial/Images/CollisionResponse.png
  25. BIN Tutorial/Images/ColumnOrderTest.png
  26. BIN Tutorial/Images/CurvedWalls.png
  27. BIN Tutorial/Images/DeathEffectTimeline.png
  28. BIN Tutorial/Images/DisableSafetyChecks.png
  29. BIN Tutorial/Images/DraggableJoystick.png
  30. BIN Tutorial/Images/EasingCurves.png
  31. BIN Tutorial/Images/FieldOfView.png
  32. BIN Tutorial/Images/FizzleFade.png
  33. BIN Tutorial/Images/FloatingJoystick.png
  34. BIN Tutorial/Images/ImprovedTextureTrace.png
  35. BIN Tutorial/Images/InlineSetterTest.png
  36. BIN Tutorial/Images/Lighting.png
  37. BIN Tutorial/Images/LineDrawing.png
  38. BIN Tutorial/Images/LineOfSight.png
  39. BIN Tutorial/Images/LinearAccessTest.png
  40. BIN Tutorial/Images/MemoryHierarchy.png
  41. BIN Tutorial/Images/MonsterDeath.png
  42. BIN Tutorial/Images/MonsterInWall.png
  43. BIN Tutorial/Images/MonsterInWall2.png
  44. BIN Tutorial/Images/MonsterMob.png
  45. BIN Tutorial/Images/MonsterSprite.png
  46. BIN Tutorial/Images/MonstersAttacking.png
  47. BIN Tutorial/Images/MuzzleFlash.png
  48. BIN Tutorial/Images/NoInliningSubscript.png
  49. BIN Tutorial/Images/OptimizedBlendTrace.png
  50. BIN Tutorial/Images/OptimizedOrderTrace.png
  51. BIN Tutorial/Images/OverlappingAccess.png
  52. BIN Tutorial/Images/PerspectiveView.png
  53. BIN Tutorial/Images/PistolOverlay.png
  54. BIN Tutorial/Images/PistolPosition.png
  55. BIN Tutorial/Images/PistolSprite.png
  56. BIN Tutorial/Images/PixelOpacityTest.png
  57. BIN Tutorial/Images/ProjectStructure.png
  58. BIN Tutorial/Images/Pythagoras.png
  59. BIN Tutorial/Images/RayFan.png
  60. BIN Tutorial/Images/RayLengths.png
  61. BIN Tutorial/Images/RayTileIntersection.png
  62. BIN Tutorial/Images/RedFlashEffect.png
  63. BIN Tutorial/Images/RedSpriteBackground.png
  64. BIN Tutorial/Images/ReleaseMode.png
  65. BIN Tutorial/Images/ReleaseModeTests.png
  66. BIN Tutorial/Images/RemoveMultiplicationTest.png
  67. BIN Tutorial/Images/RotatedOutput.png
  68. BIN Tutorial/Images/RotatedSprites.png
  69. BIN Tutorial/Images/SafetyChecksOffTest.png
  70. BIN Tutorial/Images/ScrambledOutput.png
  71. BIN Tutorial/Images/SetBaseline.png
  72. BIN Tutorial/Images/SharpBluePixel.png
  73. BIN Tutorial/Images/SlopeIntercept.png
  74. BIN Tutorial/Images/SmoothedDeathEffect.png
  75. BIN Tutorial/Images/SolidFloorColor.png
  76. BIN Tutorial/Images/SortedSprites.png
  77. BIN Tutorial/Images/SpriteBehindPlayer.png
  78. BIN Tutorial/Images/SpriteLines.png
  79. BIN Tutorial/Images/SpriteOrderBug.png
  80. BIN Tutorial/Images/SpriteRadius.png
  81. BIN Tutorial/Images/SpriteRayBug.png
  82. BIN Tutorial/Images/SpriteRayIntersection.png
  83. BIN Tutorial/Images/StagedResponse.png
  84. BIN Tutorial/Images/StateMachine.png
  85. BIN Tutorial/Images/StoredWidthTest.png
  86. BIN Tutorial/Images/StraightWalls.png
  87. BIN Tutorial/Images/StretchedWalls.png
  88. BIN Tutorial/Images/TextureLookupTrace.png
  89. BIN Tutorial/Images/TextureMapping.png
  90. BIN Tutorial/Images/TextureSmearing.png
  91. BIN Tutorial/Images/TextureVariants.png
  92. BIN Tutorial/Images/TexturedCeiling.png
  93. BIN Tutorial/Images/TexturedFloor.png
  94. BIN Tutorial/Images/TexturesAndLighting.png
  95. BIN Tutorial/Images/TileEdgeHit.png
  96. BIN Tutorial/Images/TileSteps.png
  97. BIN Tutorial/Images/Tilemap.png
  98. BIN Tutorial/Images/UnoptimizedTest.png
  99. BIN Tutorial/Images/UnoptimizedTrace.png
  100. BIN Tutorial/Images/VariedTextures.png
  101. BIN Tutorial/Images/ViewFrustum.png
  102. BIN Tutorial/Images/ViewPlane.png
  103. BIN Tutorial/Images/WalkingFrames.png
  104. BIN Tutorial/Images/WallCollisions.png
  105. BIN Tutorial/Images/WallDistance.png
  106. BIN Tutorial/Images/WallHit.png
  107. BIN Tutorial/Images/WallTexture.png
  108. +2 −2 Tutorial/Part4.md
  109. +3 −3 Tutorial/Part8.md
  110. +837 −0 Tutorial/Part9.md
7 changes: 7 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
language: swift
osx_image: xcode10.1

script:
- cd Source
- xcodebuild clean build -scheme Rampage -destination 'platform=iOS Simulator,name=iPhone XR,OS=12.1'
- xcodebuild clean test -scheme Rampage -destination 'platform=iOS Simulator,name=iPhone XR,OS=12.1'
42 changes: 26 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
## Retro Rampage

[![PayPal](https://img.shields.io/badge/paypal-donate-blue.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CR6YX6DLRNJTY&source=url)
[![Travis](https://travis-ci.org/nicklockwood/RetroRampage.svg)](https://travis-ci.org/nicklockwood/RetroRampage)
[![Swift 4.2](https://img.shields.io/badge/swift-4.2-red.svg?style=flat)](https://developer.apple.com/swift)
[![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://opensource.org/licenses/MIT)
[![Twitter](https://img.shields.io/badge/twitter-@nicklockwood-blue.svg)](http://twitter.com/nicklockwood)

![Screenshot](Tutorial/Images/MuzzleFlash.png)

### About
@@ -16,33 +22,29 @@ Modern shooters have moved on a bit from Wolfenstein's grid-based 2.5D world, bu

### Background

Ever since I first played Wolfenstein 3D on a friend's battered old 386 back in 1993 (the Mac version wasn't released until several years later) I was hooked on the *First-Person Shooter*.
Ever since I first played Wolfenstein 3D on a friend's battered old 386 back in 1993, I was hooked on the *First-Person Shooter*.

As an aspiring programmer, I wanted to recreate what I had seen. But armed only with 7th grade math and a rudimentary knowledge of BASIC, recreating the state-of-the-art in modern PC 3D graphics was hopelessly beyond my reach.

More than two decades later, a few things have changed:

* Apple has given us (well, sold us) the iPhone - a mobile computer many hundreds of times more powerful than a DOS-era desktop PC.

* Chris Lattner has given us Swift - a simple, powerful programming language with which to write apps and games.

* John Carmack has given us the Wolfenstein source code, and the wizardry behind it has been thoroughly demystified.
We have the iPhone - a mobile computer many hundreds of times more powerful than a DOS-era desktop PC; We have Swift - a simple, powerful programming language with which to write apps and games; Finally - and most importantly - we have the Wolfenstein source code, and the wizardry behind it has been thoroughly demystified.

I guess now is as good a time as any to scratch that quarter-century itch and build an FPS!

### Tutorials

The tutorials below are designed to be completed in order, and each step builds on the code from the previous one. If you decide to skip ahead, project snapshots for each step are available [here](https://github.com/nicklockwood/RetroRampage/releases).

The tutorials are written with the assumption that you are already familiar with Xcode and are comfortable setting up an iOS project and adding new files to it. No prior knowledge of the Swift language is assumed, so it's fine if you've only used Objective-C or other C-like languages.
The tutorials are written with the assumption that you are already familiar with Xcode and are comfortable setting up an iOS project and adding new files to it. No knowledge of advanced Swift features is required, so it's fine if you've only used Objective-C or other C-like languages.

[Part 1 - Separation of Concerns](Tutorial/Part1.md)

Unlike most apps, games are (or should be) highly independent of any given platform. Swift has already been ported to many platforms outside of the Apple ecosystem, including Android, Ubuntu, Windows and even Raspberry Pi. In part one we'll see how to set up our project to minimize dependencies with iOS and provide a solid foundation for writing a fully portable game engine.
Unlike most apps, games are typically designed to be independent of any given device or OS. Swift has already been ported to many platforms outside of the Apple ecosystem, including Android, Ubuntu, Windows and even Raspberry Pi. In this first part, we'll set up our project to minimize dependencies with iOS and provide a solid foundation for writing a fully portable game engine.

[Part 2 - Mazes and Motion](Tutorial/Part2.md)

Wolfenstein 3D - despite the name - is really a 2D game projected into the third dimension. The game mechanics work exactly the same as for a top-down 2D shooter, and to prove that we'll start by building the game from a top-down 2D perspective before we make the shift to first-person 3D.
Wolfenstein 3D is really a 2D game projected into the third dimension. The game mechanics work exactly the same as for a top-down 2D shooter, and to prove that we'll begin by building the game from a top-down 2D perspective before we make the shift to first-person 3D.

[Part 3 - Ray Casting](Tutorial/Part3.md)

@@ -54,31 +56,39 @@ In this chapter we'll spruce up the bare walls and floor with *texture mapping*.

[Part 5 - Sprites](Tutorial/Part5.md)

It's time to introduce some other characters to keep our player company, and display them using *sprites* - a popular technique used to add engaging content to 3D games in the days before it was possible to render textured polygonal models in real-time with sufficient detail.
It's time to introduce some monsters to keep our player company. We'll display these using *sprites* - a popular technique used to add engaging content to 3D games in the days before it was possible to render textured polygonal models in real-time with sufficient detail.

[Part 6 - Enemy Action](Tutorial/Part6.md)

Right now the monsters standing in the maze are little more than gruesome scenery. In this part we'll bring those passive monsters to life with collision detection, animations, and artificial intelligence so they can hunt and attack the player.
Right now the monsters in the maze are little more than gruesome scenery. We'll bring those passive monsters to life with collision detection, animations, and artificial intelligence so they can hunt and attack the player.

[Part 7 - Death and Pixels](Tutorial/Part7.md)

In this part we'll implement player damage, giving the monsters the ability to hurt and eventually kill the game's protagonist. We'll explore a variety of damage effects and techniques, including a cool Wolfenstein 3D death animation called *fizzlefade*.
In this part we'll implement player damage, giving the monsters the ability to hurt and eventually kill the game's protagonist. We'll explore a variety of damage effects and techniques, including a cool Wolfenstein transition called *fizzlefade*.

[Part 8 - Target Practice](Tutorial/Part8.md)

In this part we'll give the player a weapon so they can fight back against the ravenous monsters. We'll extend our drawing logic to handle screen-space sprites, add a bunch of new animations, and figure out how to implement reliable collision detection for fast-moving projectiles.
We'll now give the player a weapon so they can fight back against the ravenous monsters. This chapter will demonstrate how to extend our drawing logic to handle screen-space sprites, add a bunch of new animations, and figure out how to implement reliable collision detection for fast-moving projectiles.

[Part 9 - Performance Tuning](Tutorial/Part9.md)

More to follow!
The new effects we've added are starting to take a toll on the game's frame rate, especially on older devices. Let's take a break from adding new features and spend some time on improving the rendering speed. In this chapter we'll find out how to diagnose and fix performance bottlenecks, while avoiding the kind of micro-optimizations that will make it harder to add new features later on.

### Acknowledgments

I'd like to thank [Nat Brown](https://github.com/natbro) and [PJ Cook](https://github.com/pjcook) for their invaluable feedback on the first draft of these tutorials.

Thanks also to [Lode Vandevenne](https://github.com/lvandeve) and [Fabien Sanglard](https://github.com/fabiensanglard/), whom I've never actually spoken to, but whose brilliant explanations of ray casting and the Wolfenstein engine formed both the basis and inspiration for this tutorial series.

### Experiments

If you're up to date with the tutorials, and can't wait for the next chapter, you might like to check out some of the [experimental PRs](https://github.com/nicklockwood/RetroRampage/pulls) on Github.

These experiments demonstrate advanced features that we aren't quite ready to explore in the tutorials yet.

### Further Reading

If you've completed the tutorials and are eager to learn more, here are some resources you might find useful:
If you've exhausted the tutorials and experiments and are still eager to learn more, here are some resources you might find useful:

* [Lode's Raycasting Tutorial](https://lodev.org/cgtutor/raycasting.html#Introduction) - A great tutorial on ray casting, implemented in C++.
* [Game Engine Black Book: Wolfenstein 3D](https://www.amazon.co.uk/gp/product/1727646703/ref=as_li_tl?ie=UTF8&camp=1634&creative=6738&creativeASIN=1727646703&linkCode=as2&tag=charcoaldesig-21&linkId=aab5d43499c96f7417b7aa0a7b3e587d) - Fabien Sanglard's excellent book about the Wolfenstein 3D game engine.
@@ -87,7 +97,7 @@ If you've completed the tutorials and are eager to learn more, here are some res

### Tip Jar

I started this tutorial series thinking it would take just a few days. Nearly two months later, with no end in sight, I realize I may have been a bit naive. If you've found it interesting, please consider donating to my caffeine fund.
I started this tutorial series thinking it would take just a few days. Many months later, with no end in sight, I realize I may have been a bit naive. If you've found it interesting, please consider donating to my caffeine fund.

[![Donate via PayPal](https://www.paypalobjects.com/en_GB/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CR6YX6DLRNJTY&source=url)

69 changes: 42 additions & 27 deletions Source/Engine/Bitmap.swift
Original file line number Diff line number Diff line change
@@ -8,24 +8,23 @@

public struct Bitmap {
public private(set) var pixels: [Color]
public let width: Int
public let width, height: Int
public let isOpaque: Bool

public init(width: Int, pixels: [Color]) {
self.width = width
public init(height: Int, pixels: [Color]) {
self.height = height
self.width = pixels.count / height
self.pixels = pixels
self.isOpaque = pixels.allSatisfy { $0.isOpaque }
}
}

public extension Bitmap {
var height: Int {
return pixels.count / width
}

subscript(x: Int, y: Int) -> Color {
get { return pixels[y * width + x] }
get { return pixels[x * height + y] }
set {
guard x >= 0, y >= 0, x < width, y < height else { return }
pixels[y * width + x] = newValue
pixels[x * height + y] = newValue
}
}

@@ -35,12 +34,14 @@ public extension Bitmap {

init(width: Int, height: Int, color: Color) {
self.pixels = Array(repeating: color, count: width * height)
self.height = height
self.width = width
self.isOpaque = color.isOpaque
}

mutating func fill(rect: Rect, color: Color) {
for y in Int(rect.min.y) ..< Int(rect.max.y) {
for x in Int(rect.min.x) ..< Int(rect.max.x) {
for x in Int(rect.min.x) ..< Int(rect.max.x) {
for y in Int(rect.min.y) ..< Int(rect.max.y) {
self[x, y] = color
}
}
@@ -69,10 +70,19 @@ public extension Bitmap {
mutating func drawColumn(_ sourceX: Int, of source: Bitmap, at point: Vector, height: Double) {
let start = Int(point.y), end = Int(point.y + height) + 1
let stepY = Double(source.height) / height
for y in max(0, start) ..< min(self.height, end) {
let sourceY = (Double(y) - point.y) * stepY
let sourceColor = source[sourceX, Int(sourceY)]
blendPixel(at: Int(point.x), y, with: sourceColor)
let offset = Int(point.x) * self.height
if source.isOpaque {
for y in max(0, start) ..< min(self.height, end) {
let sourceY = (Double(y) - point.y) * stepY
let sourceColor = source[sourceX, Int(sourceY)]
pixels[offset + y] = sourceColor
}
} else {
for y in max(0, start) ..< min(self.height, end) {
let sourceY = (Double(y) - point.y) * stepY
let sourceColor = source[sourceX, Int(sourceY)]
blendPixel(at: offset + y, with: sourceColor)
}
}
}

@@ -86,14 +96,21 @@ public extension Bitmap {
}
}

private 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
)
private mutating func blendPixel(at index: Int, with newColor: Color) {
switch newColor.a {
case 0:
break
case 255:
pixels[index] = newColor
default:
let oldColor = pixels[index]
let inverseAlpha = 1 - Double(newColor.a) / 255
pixels[index] = 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
)
}
}

mutating func tint(with color: Color, opacity: Double) {
@@ -104,10 +121,8 @@ public extension Bitmap {
b: UInt8(Double(color.b) * alpha),
a: UInt8(255 * alpha)
)
for y in 0 ..< height {
for x in 0 ..< width {
blendPixel(at: x, y, with: color)
}
for i in pixels.indices {
blendPixel(at: i, with: color)
}
}
}
4 changes: 4 additions & 0 deletions Source/Engine/Color.swift
Original file line number Diff line number Diff line change
@@ -18,6 +18,10 @@ public struct Color: Codable {
}

public extension Color {
var isOpaque: Bool {
return a == 255
}

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)
4 changes: 2 additions & 2 deletions Source/Engine/Renderer.swift
Original file line number Diff line number Diff line change
@@ -128,8 +128,8 @@ public extension Renderer {
bitmap.tint(with: effect.color, opacity: effect.progress)
case .fizzleOut:
let threshold = Int(effect.progress * Double(fizzle.count))
for y in 0 ..< bitmap.height {
for x in 0 ..< bitmap.width {
for x in 0 ..< bitmap.width {
for y in 0 ..< bitmap.height {
let granularity = 4
let index = y / granularity * bitmap.width + x / granularity
let fizzledIndex = fizzle[index % fizzle.count]
Loading