Skip to content

Instantly share code, notes, and snippets.

@JohnSundell
Created July 8, 2020 22:41
Show Gist options
  • 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)
}
}
@ivoryrolland95
Copy link

ivoryrolland95 commented Jul 31, 2025

i really respect people who can make games, even simple ones. i’m just starting to learn game development myself, and i want to make something like aviator. just to be clear, i mean the kind of games you can find in the casino apps listed here http://aviatorapps.in/. or something similar to aviatrix, which you can read about here https://aviatrix.com.in/apps/. these aren’t super simple games and you really need a team to build something like that. but i want to try making a simplified version on my own.

@bazz221
Copy link

bazz221 commented Aug 11, 2025

John Sundell-in GitHub-dəki Gist-ləri çox faydalıdır və xüsusən Swift və iOS inkişafı ilə maraqlananlar üçün çox yaxşı bir qaynaqdır. O, effektiv və funksional tətbiqlərin yaradılmasına kömək edir. Eyni zamanda, mən də esport sahəsindəki yenilikləri izləyirəm. Məsələn, az-esports saytında FaZe Clan-ın IEM Dallas 2025-dəki 1xbet azeri https://daylinews-az.com/celsinin-dircelisi-guclu-psj-iti-disli-braziliyalilar-yay-movsumunun-esas-klub-turnirinden-en-vacib-neticeler/ debütü barədə oxudum. Bu, müxtəlif sahələrdə – texnologiya və esport – necə inkişaf etməyin vacibliyini göstərir.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment