Occasionally bugs happen, and given the episodic nature of this tutorial, it is difficult to address these retrospectively without changing the Git commit history.
This file is a record of bugs that have been found and fixed since the tutorial started. The dates next to each bug indicate when the fix was merged. If you completed the relevant tutorial(s) after the date listed for a given bug, you can safely ignore it.
The original push-wall update logic contained a bug that meant if you tried to push the wall in a direction it couldn't move, it would start to move and then stop one frame later. Visually, this isn't a problem, however it meant that the start and stop sounds would play continuously for as long as you were in contact with the wall.
The fix for this was to replace the following lines in Pushwall.swift
:
if abs(intersection.x) > abs(intersection.y) {
velocity = Vector(x: intersection.x > 0 ? speed : -speed, y: 0)
} else {
velocity = Vector(x: 0, y: intersection.y > 0 ? speed : -speed)
}
with:
let direction: Vector
if abs(intersection.x) > abs(intersection.y) {
direction = Vector(x: intersection.x > 0 ? 1 : -1, y: 0)
} else {
direction = Vector(x: 0, y: intersection.y > 0 ? 1 : -1)
}
if !world.map.tile(at: position + direction, from: position).isWall {
velocity += direction * speed
}
This ensures that the push-wall velocity remains zero if it's not able to move, so the sounds do not play.
In Part 11 we added logic to set the tile beneath a push-wall by finding the nearest floor tile value, however there was a bug in the implementation that meant the sampled value was never actually used. To fix this, replace the following line in the reset()
function in World.swift
:
map[x, y] = .floor
with:
map[x, y] = map.closestFloorTile(to: x, y) ?? .floor
Then change the following line just below it:
tile = map.closestFloorTile(to: x, y) ?? .wall
to:
tile = .wall
The original corner texture fix in Part 18 sampled from the wrong texture edge in some cases. To fix this, replace the following code in Renderer.swift
:
if world.map[neighborX, tileY].isWall {
wallTexture = textures[tile.textures[1]]
} else {
let isDoor = world.isDoor(at: neighborX, tileY)
wallTexture = textures[isDoor ? .doorjamb : tile.textures[0]]
}
wallX = end.y - end.y.rounded(.down)
with:
if world.map[neighborX, tileY].isWall {
wallTexture = textures[tile.textures[1]]
wallX = end.x - end.x.rounded(.down)
} else {
let isDoor = world.isDoor(at: neighborX, tileY)
wallTexture = textures[isDoor ? .doorjamb : tile.textures[0]]
wallX = end.y - end.y.rounded(.down)
}
The original code for Part 16 disabled the status bar and portrait mode in the Info.plist
, but this isn't actually sufficient to disable it on iPad. To do so properly you need to also check the "Require full screen" option, and add the following lines to ViewController
:
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscape
}
override var prefersStatusBarHidden: Bool {
return true
}
The original drawColumn()
method added in Part 4 had an unsafe upper bound that could potentially try to read a negative index in the wall texture, resulting in a crash.
The fix was to replace the following line in the drawColumn()
method in Bitmap.swift
:
let sourceY = (Double(y) - point.y) * stepY
with:
let sourceY = max(0, Double(y) - point.y) * stepY
Note that this line appears twice in the latest version of drawColumn()
due to the isOpaque
optimization added in Part 9. You should replace both occurences.
The original ceiling texture code we added in Part 4 resulted in a one-pixel gap at the top of the ceiling texture.
The fix for this was to replace the following line in the // Draw wall
section of Renderer.draw()
:
let wallStart = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 + 0.001)
with:
let wallStart = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 - 0.001)
Then to replace the following line in the // Draw floor and ceiling
section:
bitmap[x, bitmap.height - y] = ceilingTexture[normalized: textureX, textureY]
with:
bitmap[x, bitmap.height - 1 - y] = ceilingTexture[normalized: textureX, textureY]
When we added the shotgun in Part 14 there was a bug in the weapon switching logic which meant that if you fired the last round in the shotgun as you exited the level you'd begin the next level still with the shotgun, but no ammo and no way to switch back to the pistol.
The fix was to replace the following lines near the bottom of the Player.update()
method:
switch state {
case .idle:
break
case .firing:
if animation.isCompleted {
state = .idle
animation = weapon.attributes.idleAnimation
if ammo == 0 {
setWeapon(.pistol)
}
}
}
with:
switch state {
case .idle:
if ammo == 0 {
setWeapon(.pistol)
}
case .firing:
if animation.isCompleted {
state = .idle
animation = weapon.attributes.idleAnimation
}
}
When we originally wrote the Player weapon code in Part 8 we added a lastAttackTime
property which was not actually used in the implementation.
This has now been removed.
When push-walls were introduced in Part 11, the World.hitTest()
method was not updated to detect ray intersections with the Pushwall
billboards, with the result that the monster in the second room in the first level can see (and be shot by) the player through the push-wall.
The fix was to replace the following lines in the World.hitTest()
method:
for door in doors {
guard let hit = door.billboard.hitTest(ray) else {
with:
let billboards = doors.map { $0.billboard } +
pushwalls.flatMap { $0.billboards(facing: ray.origin) }
for billboard in billboards {
guard let hit = billboard.hitTest(ray) else {
The original drawColumn()
method introduced in Part 4 had an unsafe upper bound that could potentially cause a crash by trying to read beyond the end of the source bitmap.
The fix was to replace the following line in the drawColumn()
method in Bitmap.swift
:
let start = Int(point.y), end = Int(point.y + height) + 1
with:
let start = Int(point.y), end = Int((point.y + height).rounded(.up))
The original logic in Part 9 that switched to column-first pixel order had a bug where the width and height were swapped on output, causing the result to be corrupted for non-square images. Since the game used square textures for all the walls and sprites, the bug wasn't immediately apparent.
The fix was to change the last line in the Bitmap.init()
function in UIImage+Bitmap.swift
from:
self.init(height: cgImage.width, pixels: pixels)
to:
self.init(height: cgImage.height, pixels: pixels)
The original logic in Part 9 for rotating the textures to compensate for switching to column-first pixel order had the side-effect of flipping the Z-axis. This resulted in the floor texture being drawn on the ceiling, and vice-versa (thanks to Adam McNight for reporting).
The fix for this was to change two lines in UIImage+Bitmap.swift
. First, in UIImage.init()
change:
self.init(cgImage: cgImage, scale: 1, orientation: .left)
to:
self.init(cgImage: cgImage, scale: 1, orientation: .leftMirrored)
Then in Bitmap.init()
change:
UIImage(cgImage: cgImage, scale: 1, orientation: .rightMirrored).draw(at: .zero)
to:
UIImage(cgImage: cgImage, scale: 1, orientation: .left).draw(at: .zero)
The original wall collision detection code described in Part 2 had a bug that could cause the player to stick when sliding along a wall (thanks to José Ibañez for reporting).
The fix for this was to return the largest intersection detected between any wall segment, rather than just the first intersection detected. The necessary code changes are in Actor.intersection(with map:)
, which should now look like this:
func intersection(with map: Tilemap) -> Vector? {
let minX = Int(rect.min.x), maxX = Int(rect.max.x)
let minY = Int(rect.min.y), maxY = Int(rect.max.y)
var largestIntersection: Vector?
for y in minY ... maxY {
for x in minX ... maxX where map[x, y].isWall {
let wallRect = Rect(
min: Vector(x: Double(x), y: Double(y)),
max: Vector(x: Double(x + 1), y: Double(y + 1))
)
if let intersection = rect.intersection(with: wallRect),
intersection.length > largestIntersection?.length ?? 0 {
largestIntersection = intersection
}
}
}
return largestIntersection
}
In the original version of Part 5 there were a couple of bugs in the sprite texture coordinate calculation. In your own project, check if the // Draw sprites
section in Renderer.swift
contains the following two lines:
let textureX = Int(spriteX * Double(wallTexture.width))
let spriteTexture = textures[sprite.texture]
If so, replace them with:
let spriteTexture = textures[sprite.texture]
let textureX = min(Int(spriteX * Double(spriteTexture.width)), spriteTexture.width - 1)