Created
July 8, 2020 22:41
-
-
Save JohnSundell/7ae3223b5bad3712378a57aaff31d7e2 to your computer and use it in GitHub Desktop.
A simple game written in SwiftUI. Note that this is just a fun little hack, the code is not meant to be taken seriously, and only works on iPhones in portrait mode.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A fun little game written in SwiftUI | |
// Copyright (c) John Sundell 2020, MIT license. | |
// This is a hacky implementation written just for fun. | |
// It's only verified to work on iPhones in portrait mode. | |
import SwiftUI | |
final class GameController: ObservableObject { | |
@Published var plane = GameObject.plane() | |
@Published private(set) var clouds = [GameObject]() | |
@Published private(set) var stars = [GameObject]() | |
@Published private(set) var score = 0 | |
var movement: Movement? | |
private var lastCloudSpawnDate = Date() | |
private var lastStarSpawnDate = Date() | |
private var displayLink: CADisplayLink? | |
func activate() { | |
guard displayLink == nil else { return } | |
let link = CADisplayLink(target: self, selector: #selector(update)) | |
link.preferredFramesPerSecond = 60 | |
link.add(to: .main, forMode: .common) | |
displayLink = link | |
} | |
@objc private func update() { | |
switch movement?.direction { | |
case nil: | |
break | |
case .leading: | |
plane.offset.x -= plane.speed | |
case .trailing: | |
plane.offset.x += plane.speed | |
} | |
plane.offset.x = max(0.1, min(plane.offset.x, 0.9)) | |
let currentDate = Date() | |
if currentDate.timeIntervalSince(lastCloudSpawnDate) > 1.5 { | |
clouds.append(.cloud()) | |
lastCloudSpawnDate = currentDate | |
} | |
if currentDate.timeIntervalSince(lastStarSpawnDate) > 3 { | |
stars.append(.star()) | |
lastStarSpawnDate = currentDate | |
} | |
moveGameObjects(\.clouds) | |
moveGameObjects(\.stars) | |
let collisionThreshold: CGFloat = 0.06 | |
stars = stars.filter { star in | |
let contact = ( | |
x: abs(plane.offset.x - star.offset.x) < collisionThreshold, | |
y: abs(plane.offset.y - star.offset.y) < collisionThreshold | |
) | |
guard contact.x, contact.y else { | |
return true | |
} | |
score += 100 | |
return false | |
} | |
} | |
private func moveGameObjects( | |
_ keyPath: ReferenceWritableKeyPath<GameController, [GameObject]> | |
) { | |
self[keyPath: keyPath] = self[keyPath: keyPath].compactMap { | |
var object = $0 | |
object.offset.y += object.speed | |
return object.offset.y < 1.1 ? object : nil | |
} | |
} | |
} | |
struct Movement { | |
enum Direction { | |
case leading, trailing | |
} | |
var direction: Direction? = nil | |
var location: CGPoint | |
} | |
struct Game: View { | |
@ObservedObject var controller: GameController | |
var body: some View { | |
GeometryReader { proxy in | |
ForEach(controller.clouds) { cloud in | |
cloud.renderedInContainer(ofSize: proxy.size) | |
} | |
ForEach(controller.stars) { star in | |
star.renderedInContainer(ofSize: proxy.size) | |
} | |
ZStack(alignment: .top) { | |
HStack { | |
Spacer() | |
Text("Score: \(controller.score)") | |
.bold() | |
.foregroundColor(.black) | |
Spacer() | |
} | |
.padding(.top) | |
} | |
controller.plane | |
.renderedInContainer(ofSize: proxy.size) | |
} | |
.background(Color(#colorLiteral(red: 0, green: 0.7216904445, blue: 1, alpha: 1)).edgesIgnoringSafeArea(.all)) | |
.onAppear(perform: controller.activate) | |
.gesture(gesture) | |
} | |
private var gesture: some Gesture { | |
DragGesture(minimumDistance: 0) | |
.onChanged { state in | |
guard var movement = controller.movement else { | |
controller.movement = Movement(location: state.location) | |
return | |
} | |
let delta = state.location.x - movement.location.x | |
let threshold: CGFloat = 5 | |
if delta > threshold { | |
movement.direction = .trailing | |
} else if delta < -threshold { | |
movement.direction = .leading | |
} | |
movement.location = state.location | |
controller.movement = movement | |
} | |
.onEnded { _ in | |
controller.movement = nil | |
} | |
} | |
} | |
struct GameObject: View, Identifiable { | |
let id = UUID() | |
var spriteName: String | |
var color: Color | |
var speed: CGFloat | |
var scale: CGFloat | |
var rotation = Angle(degrees: 0) | |
var offset = Self.randomStartOffset() | |
var body: some View { | |
Image(systemName: spriteName) | |
.resizable() | |
.aspectRatio(contentMode: .fit) | |
.foregroundColor(color) | |
.rotationEffect(rotation) | |
} | |
func renderedInContainer(ofSize size: CGSize) -> some View { | |
position( | |
x: offset.x * size.width, | |
y: offset.y * size.height | |
) | |
.frame(width: size.width * scale) | |
} | |
} | |
extension GameObject { | |
static func plane() -> Self { | |
GameObject( | |
spriteName: "airplane", | |
color: .black, | |
speed: 0.005, | |
scale: 0.1, | |
rotation: Angle(degrees: -90), | |
offset: (0.5, 0.9) | |
) | |
} | |
static func cloud() -> Self { | |
GameObject( | |
spriteName: "icloud.fill", | |
color: .white, | |
speed: 0.002, | |
scale: 0.3 | |
) | |
} | |
static func star() -> Self { | |
GameObject( | |
spriteName: "star.fill", | |
color: .yellow, | |
speed: 0.005, | |
scale: 0.1 | |
) | |
} | |
private static func randomStartOffset() -> (x: CGFloat, y: CGFloat) { | |
(.random(in: 0..<1), -0.1) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment