Skip to content

Instantly share code, notes, and snippets.

@JohnSundell
Created July 8, 2020 22:41
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save JohnSundell/7ae3223b5bad3712378a57aaff31d7e2 to your computer and use it in GitHub Desktop.
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.
// 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