SwiftUI track drawing weird

44 Views Asked by At

I want to write a simple game in SwiftUI where the user can drag a circle around to dodge trees and it should draw a track of where it was however the track is really weird and rectangular instead of a smooth curve. Can anyone help me?

Here’s my code:

import SwiftUI

struct ContentView: View {

@State private var track = [CGFloat]()

@State private var offset: CGFloat = 0
let timer = Timer.publish(every: 0.001, on: .current, in: .common).autoconnect()

var body: some View {
    GeometryReader { geo in
        ZStack {
            Color.black
            Path { path in
                
                let height = (geo.size.height / 2) / CGFloat(track.count)
                
                
                
                for i in 0..<track.count {
                    path.addLine(to: CGPoint(x: geo.size.width * track[i], y: height * CGFloat(i)))
                    
                    path.move(to: CGPoint(x: geo.size.width * track[i], y: height * CGFloat(i)))
                }
            } .stroke(lineWidth: 1)
                .foregroundStyle(.yellow)
                
            Circle()
                .frame(width: 25)
                .foregroundStyle(.red)
                .position(x: offset, y: geo.size.height / 2)
                .gesture (
                    DragGesture()
                        .onChanged { value in
                            offset = min(geo.size.width - 15, max(15 ,value.location.x))
                        }
                ) 
                .onAppear {
                    offset = geo.size.width / 2
                }
                .onReceive(timer, perform: { _ in
                    track.append((offset / geo.size.width))
                    if track.count >= 100 {
                        track.removeFirst()
                    }
                })
            
        }
    }
}
}
1

There are 1 best solutions below

0
Sweeper On BEST ANSWER

The timer publishes too fast, faster than how fast the Gesture updates its value.

Every 0.001 seconds, you take a sample of the circle's offset. Many of these samples would likely be the same value, which makes the track look "rectangular".

Note that the rate of the timer also represents the speed of the circle, you only display 100 samples of them on the top half of the screen. This means every 0.1 seconds, the circle would "move vertically" by geo.size.height / 2. I'm not sure if you want a circle that fast.

I would recommend a timer interval around the rate at which the gesture updates. A value like 1 / 30 works fine in my experiments.

Also, your drawing code is a bit incorrect. I would calculate all the points first, and use addLines. You should draw tracks in the reverse order, and from bottom to top.

Path { path in
    let height = (geo.size.height / 2) / 100
    let points = track.reversed().enumerated().map { (i, value) in
        CGPoint(x: geo.size.width * value, y: geo.size.height / 2 - height * CGFloat(i))
    }
    path.addLines(points)
}
// a round line join might look better than the default miter
.stroke(style: StrokeStyle(lineJoin: .round))
.foregroundStyle(.yellow)

With your current control, you cannot freely control the vertical speed of the circle.

A more flexible approach would be to record the time as well as the offset in the gesture's onChanged. Instead of storing a constant 100 of these, store everything with a time within the past X seconds.

When drawing the line, you would use the difference between timestamps to figure out how much the circle would have moved vertically in that period (according to a speed that you choose). This gives you the difference in y coordinates between the points.

Here is a rough sketch:

struct Sample: Hashable {
    
    let timestamp: Date
    let offset: CGFloat
}

struct ContentView: View {
    
    @State private var track = [Sample]()
    
    let timer = Timer.publish(every: 1 / 30, on: .current, in: .common).autoconnect()
    
    let speed: CGFloat = 100 // points per second
    
    var body: some View {
        GeometryReader { geo in
            ZStack {
                Color.black
                Path { path in
                    var points = [CGPoint]()
                    var currentY = geo.size.height / 2
                    let reversed = track.sorted(using: KeyPathComparator(\.timestamp, order: .reverse))
                    for (sample, nextSample) in zip(reversed, reversed.dropFirst()) {
                        points.append(CGPoint(x: sample.offset, y: currentY))
                        let elapsedTime = sample.timestamp.timeIntervalSince(nextSample.timestamp)
                        currentY -= speed * elapsedTime
                    }
                    if let last = reversed.last {
                        points.append(CGPoint(x: last.offset, y: currentY))
                    }
                    path.addLines(points)
                }
                .stroke(style: StrokeStyle(lineJoin: .round))
                .foregroundStyle(.yellow)
                
                Circle()
                    .frame(width: 25)
                    .foregroundStyle(.red)
                    .position(x: track.last?.offset ?? geo.size.width / 2, y: geo.size.height / 2)
                    .gesture (
                        DragGesture()
                            .onChanged { value in
                                let offset = min(geo.size.width - 15, max(15 ,value.location.x))
                                track.append(Sample(timestamp: Date(), offset: offset))
                            }
                    )
                    .onAppear {
                        track = [Sample(timestamp: Date(), offset: geo.size.width / 2)]
                    }
                    .onReceive(timer, perform: { time in
                        if let lastSample = track.last,
                            time.timeIntervalSince(lastSample.timestamp) >= 1 / 30 {
                            track.append(Sample(timestamp: time, offset: lastSample.offset))
                        }
                        let timeThreshold = geo.size.height / 2 / speed + 0.1 // add 0.1 seconds just to be safe
                        track.removeAll(where: { time.timeIntervalSince($0.timestamp) > timeThreshold })
                    })
                
            }
        }
    }
}