In the fps im making, I am now working on mapping the levels. I have already made the walls and provided texture on them, and now I'm doing the same to the floor. I am able to code a red floor, however whenever I try to code texture on it I get the following error:
"Thread 1: Swift runtime failure: Unexpectedly found nil while unwrapping an Optional value"
The code appears at this line in ViewController.swift
return Textures(loader: { name in Bitmap(image: UIImage(named: name)!)!})
Here's the full code for ViewController.swift:
import Engine
private let joystickRadius: Double = 40
private let maximumTimeStep: Double = 1 / 20
private let worldTimeStep: Double = 1 / 120
private func loadTextures() -> Textures
{
return Textures(loader: { name in Bitmap(image: UIImage(named: name)!)!})
}
private func loadMap() -> Tilemap {
let jsonURL = Bundle.main.url(forResource: "Map", withExtension: "json")!
let jsonData = try! Data(contentsOf: jsonURL)
return try! JSONDecoder().decode(Tilemap.self, from: jsonData)
}
class ViewController: UIViewController {
private let imageView = UIImageView()
private let panGesture = UIPanGestureRecognizer()
private var world = World(map: loadMap())
private var lastFrameTime = CACurrentMediaTime()
private let textures = loadTextures()
override func viewDidLoad() {
super.viewDidLoad()
setUpImageView()
let displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink.add(to: .main, forMode: .common)
view.addGestureRecognizer(panGesture)
}
private var inputVector: Vector {
switch panGesture.state {
case .began, .changed:
let translation = panGesture.translation(in: view)
var vector = Vector(x: Double(translation.x), y: Double(translation.y))
vector /= max(joystickRadius, vector.length)
panGesture.setTranslation(CGPoint(
x: vector.x * joystickRadius,
y: vector.y * joystickRadius
), in: view)
return vector
default:
return Vector(x: 0, y: 0)
}
}
@objc func update(_ displayLink: CADisplayLink) {
let timeStep = min(maximumTimeStep, displayLink.timestamp - lastFrameTime)
let inputVector = self.inputVector
let rotation = inputVector.x * world.player.turningSpeed * worldTimeStep
let input = Input(speed: -inputVector.y, rotation: Rotation(sine: sin(rotation), cosine: cos(rotation)))
let worldSteps = (timeStep / worldTimeStep).rounded(.up)
for _ in 0 ..< Int(worldSteps) {
world.update(timeStep: timeStep / worldSteps, input: input)
}
lastFrameTime = displayLink.timestamp
let width = Int(imageView.bounds.width), height = Int(imageView.bounds.height)
var renderer = Renderer(width: width, height: height, textures: textures)
renderer.draw(world)
imageView.image = UIImage(bitmap: renderer.bitmap)
}
func setUpImageView() {
view.addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
imageView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
imageView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
imageView.contentMode = .scaleAspectFit
imageView.backgroundColor = .black
imageView.layer.magnificationFilter = .nearest
}
}
Here's the code for other files that deal with the texturing of the floor and the level in general:
Textures.swift
{
case wall, wall2
case floor, ceiling
}
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]!
}
}
Renderer.swift:
public private(set) var bitmap: Bitmap
private let textures: Textures
public init(width: Int, height: Int, textures: Textures) {
self.bitmap = Bitmap(width: width, height: height, color: .black)
self.textures = textures
}
}
public extension Renderer {
mutating func draw(_ world: World) {
let focalLength = 1.0
let viewWidth = Double(bitmap.width) / Double(bitmap.height)
let viewPlane = world.player.direction.orthogonal * viewWidth
let viewCenter = world.player.position + world.player.direction * focalLength
let viewStart = viewCenter - viewPlane / 2
// Cast rays
let columns = bitmap.width
let step = viewPlane / Double(columns)
var columnPosition = viewStart
for x in 0 ..< columns {
let rayDirection = columnPosition - world.player.position
let viewPlaneDistance = rayDirection.length
let ray = Ray(
origin: world.player.position,
direction: rayDirection / viewPlaneDistance
)
let end = world.map.hitTest(ray)
let wallDistance = (end - ray.origin).length
// Draw wall
let wallHeight = 1.0
let distanceRatio = viewPlaneDistance / focalLength
let perpendicular = wallDistance / distanceRatio
let height = wallHeight * focalLength / perpendicular * Double(bitmap.height)
let wallTexture: Bitmap
let wallX: Double
if end.x.rounded(.down) == end.x
{
wallTexture = textures[.wall]
wallX = end.y - end.y.rounded(.down)
}
else
{
wallTexture = textures[.wall2]
wallX = end.x - end.x.rounded(.down)
}
let textureX = Int(wallX * Double(wallTexture.width))
let wallStart = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 - 0.001)
bitmap.drawColumn(textureX, of: wallTexture, at: wallStart, height: height)
// Draw floor
let floorTexture = textures[.floor]
let floorStart = Int(wallStart.y + height) + 1
for y in min(floorStart, bitmap.height) ..< bitmap.height {
let normalizedY = (Double(y) / Double(bitmap.height)) * 2 - 1
let perpendicular = wallHeight * focalLength / normalizedY
let distance = perpendicular * distanceRatio
let mapPosition = ray.origin + ray.direction * distance
let tileX = mapPosition.x.rounded(.down), tileY = mapPosition.y.rounded(.down)
let textureX = Int((mapPosition.x - tileX) * Double(floorTexture.width))
let textureY = Int((mapPosition.y - tileY) * Double(floorTexture.height))
bitmap[x, y] = floorTexture[textureX, textureY]
}
columnPosition += step
}
}
}
The rest of the code can be found in the link below: https://github.com/KingDecorpel12/RampageFPS/tree/main/RetroRampage/Source
Any and all help will be greatly appreciated!
look at the Debug navigator
Debug Navigator is quite useless when finding which is being nil and you may need to modify your code:
private func loadTextures() -> Textures {
return Textures(loader: { name in
guard let image = UIImage(named: name) else {
fatalError("UIImage for \(name) cannot be created")
}
guard let bitmap = Bitmap(image: image) else {
fatalError("Bitmap for \(name) (image: \(image)) cannot be created")
}
return bitmap
})
}
Generally, you use toooo.... many forced unwrappings (!
) in your code. I recommend you to learn safe ways to work with Optionals.