Основная графика с неожиданным поведением DisplayLink

#swift #core-graphics

Вопрос:

Я пытаюсь изучить основную графику, и у меня возникают проблемы с пониманием поведения написанного мной кода, в котором используется подклассный UIView и переопределение draw(_ rect:) функции.

Я написал базовую демо-версию прыгающего мяча. Любое количество случайных шаров создается со случайным положением и скоростью. Затем они подпрыгивают по экрану.

Моя проблема в том, что движение шаров кажется неожиданным, и в нем много мерцания. Вот последовательность внутри для циклов, чтобы повторить все шары:

  1. Проверьте, нет ли столкновений.
  2. Если произойдет столкновение со стеной, умножьте скорость на -1.
  3. Увеличьте положение шара на скорость шара.

В настоящее время я не очищаю контекст, поэтому я ожидаю, что существующие шары останутся на месте. Вместо этого они, кажется, плавно скользят вместе с движущимся мячом. Я хотел бы понять, почему это так.

Вот изображение того, как текущий код выполняется со скоростью 4 кадра в секунду, чтобы вы могли видеть, как рисуются фигуры и как они перемещаются вперед и назад:

захват экрана приложения iOS с 5 шариками, которые случайным образом отображаются на экране со случайными скоростями и положениями.

Вот мой код:

     class ViewController: UIViewController {
    
    let myView = MyView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBlue
        myView.translatesAutoresizingMaskIntoConstraints = false
        
        view.addSubview(myView)
        
        NSLayoutConstraint.activate([
            myView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            myView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            myView.widthAnchor.constraint(equalTo: view.widthAnchor),
            myView.heightAnchor.constraint(equalTo: view.heightAnchor)
        ])
        
        createDisplayLink(fps: 60)
    }
    
    func createDisplayLink(fps: Int) {
        let displaylink = CADisplayLink(target: self,
                                        selector: #selector(step))
        
        displaylink.preferredFramesPerSecond = fps
        
        displaylink.add(to: .current,
                        forMode: RunLoop.Mode.default)
    }
    
    @objc func step(displaylink: CADisplayLink) {
        myView.setNeedsDisplay()
    }
}

class MyView: UIView {
    
    let numBalls = 5
    var balls = [Ball]()
    
    override init(frame:CGRect) {
        super.init(frame:frame)
        
        for _ in 0..<numBalls {
            balls.append(
                Ball(
                    ballPosition: Vec2(x: CGFloat.random(in: 0...UIScreen.main.bounds.width), y: CGFloat.random(in: 0...UIScreen.main.bounds.height)),
                    ballSpeed: Vec2(x: CGFloat.random(in: 0.5...2), y: CGFloat.random(in: 0.5...2))))
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        for i in 0..<numBalls {
            if balls[i].ballPosition.x > self.bounds.width - balls[i].ballSize || balls[i].ballPosition.x < 0 {
                balls[i].ballSpeed.x *= -1
            }
            
            balls[i].ballPosition.x  = balls[i].ballSpeed.x
            
            if balls[i].ballPosition.y > self.bounds.height - balls[i].ballSize || balls[i].ballPosition.y < 0 {
                balls[i].ballSpeed.y *= -1
                
            }
            balls[i].ballPosition.y  = balls[i].ballSpeed.y
        }
        
        for i in 0..<numBalls {
            context.setFillColor(UIColor.red.cgColor)
            context.setStrokeColor(UIColor.green.cgColor)
            context.setLineWidth(0)
            
            let rectangle = CGRect(x: balls[i].ballPosition.x, y: balls[i].ballPosition.y, width: balls[i].ballSize, height: balls[i].ballSize)
            context.addEllipse(in: rectangle)
            context.drawPath(using: .fillStroke)
        }
    }
}
 

Комментарии:

1. UIKit dynamics или SpriteKit были бы лучшим инструментом.

2. Я понимаю. Я работаю над фреймворком, который возложит ответственность за управление графикой на программиста. Это в образовательных целях. Мне нужно, чтобы мои пользователи сами контролировали очистку экрана и анимацию.

Ответ №1:

Здесь много недоразумений, поэтому я постараюсь разобраться в них по одному:

  • CADisplayLink не обещает, что он будет вызывать ваш пошаговый метод каждые 1/60 секунды. Есть причина, по которой свойство называется предпочтительными кадрами в секунду. Это просто подсказка системе о том, чего бы вы хотели. Он может звонить вам реже, и в любом случае будет некоторое количество ошибок.
  • Чтобы выполнить свои собственные анимации вручную, вам нужно посмотреть, какое время на самом деле привязано к данному кадру, и использовать его, чтобы определить, где что находится. Ссылка на cadisplay включает в себя a timestamp , чтобы вы знали об этом. Вы не можете просто увеличивать скорость. Вам нужно умножить скорость на фактическое время, чтобы определить расстояние.
  • «В настоящее время я не проясняю контекст, поэтому я бы ожидал, что существующие шары останутся на месте». Каждый раз draw(rect:) , когда вам звонят, вы получаете новый контекст. Вы несете ответственность за то, чтобы нарисовать все для текущего кадра. Между кадрами нет постоянства. (Основная анимация обычно предоставляет такие функции за счет эффективного объединения слоев; но вы решили использовать основную графику, и там вам нужно каждый раз рисовать все. Как правило, мы не используем основную графику таким образом.)
  • myView.setNeedsDisplay() это не значит «нарисуйте этот кадр прямо сейчас». Это означает: «в следующий раз, когда вы будете рисовать, этот вид нужно будет перерисовать». В зависимости от того, когда именно сработает ссылка Cadisplay, вы можете удалить кадр или нет. Используя основную графику, вам нужно будет обновить все местоположения круга перед вызовом setNeedsDisplay() . Тогда draw(rect:) следует просто нарисовать их, а не вычислять, какие они. (Однако CADisplayLink действительно предназначен для работы с калибровочными слоями, и рисунок NSView не предназначен для частого обновления, поэтому все еще может быть немного сложно сохранять плавность.)

Более нормальным способом создания этой системы было бы создать слой CAShapeLayer для каждого шара и расположить их на слое NSView. Затем в обратном вызове CADisplayLink вы бы скорректировали их позиции на основе метки времени следующего кадра. С другой стороны, вы можете просто настроить повторяющийся NSTimer или DispatchTimerSource (а не ссылку Cadisplay) со скоростью значительно ниже скорости обновления экрана (например, 1/20 с) и переместить позиции слоев в этом обратном вызове. Это было бы красиво и просто и позволило бы избежать сложностей, связанных с CADisplayLink (который намного мощнее, но ожидает, что вы будете использовать метку времени и учитывать другие проблемы с программным обеспечением в реальном времени).

Комментарии:

1. Вы не очищаете вид в первую очередь. То, что вам дают, не обещает быть инициализированной памятью (это было бы пустой тратой времени, когда вы все равно все рисовали). Обычно вы делаете что-то вроде NSColor.black.set; NSBezierPath.fill(rect) . (Это просто заполняет «грязный» прямоугольник, который является единственной частью, которую вам абсолютно необходимо нарисовать. Вместо этого вы могли бы заполнить свои границы.)

2. Основные графические объекты по возможности перерабатываются. Поэтому я не удивлюсь, если получу обратно CGContext, который я только что нарисовал пару кадров назад (может быть, даже три кадра). Каким бы ни был цикл, он, скорее всего, повторится, поэтому, если есть два контекста, вы бы поместили каждый четный кадр в один, а каждый нечетный-в другой. Учитывая ваше перекрытие, вы бы увидели, что это смещается вперед и назад (из-за того, где рисуется первый круг). Вы увидите такое же поведение для трехкадрового цикла.

3. Однако я бы не особенно ожидал, что контекст, который я только что использовал, вернется в следующем кадре (учитывая скорость, с которой вы это делаете). Вероятно, это не попало бы в корзину, пока вы не попросили другой контекст. Если бы вы рисовали намного медленнее, это было бы более возможным и создавало бы иллюзию, что CGContext-это постоянный холст. (Я размываю значение «CGContext» здесь и «внутреннее резервное хранилище, в которое включается контекст». Но это очень разные вещи, так что не путайте их.)

4. Однако я настоятельно рекомендую вам изучить основную анимацию и, в частности, CALayer. Я верю, что это послужит лучшей основой для этого образовательного инструмента. Слои ведут себя немного больше как ваша мысленная модель (не полностью; они все еще сложны и имеют много субтитров, но ближе). Вы все еще можете полностью выполнять анимацию вручную, используя слои, и рисовать с помощью CoreGraphics.

5. Ты действительно хорошо справляешься здесь, копаясь во всех мелочах и деталях. Я ожидаю, что к тому времени, когда вы закончите, вы получите довольно четкое представление о системе. Чтобы узнать больше о базовой графике, я рекомендую developer.apple.com/library/archive/documentation/… и для основной анимации developer.apple.com/library/archive/documentation/Cocoa/…