Please note, this blog entry is from a previous course. You might want to check out the current one.
Task #1
Create a straightforward Breakout game using Dynamic Animator. See the image in the Screen Shots section below to get a general idea of the game, but your UI should not look exactly like the Screen Shot. Creativity is encouraged and will be rewarded.
Hint #1
This assignment is intentionally vague about the feature set of your game to make room for you to show us some creativity. …
… that’s really vague. The first task is creating the “nearly” complete game. Therefore my posts will be a little bit different – not synchronous with the tasks, but in steps that seem sensible to me at the time. I will not finish the complete assignment and than return create the posts, therefore it might be that I will jump back and forth, and maybe change parts from one step in a later one …
Let’s start. Create a new project. Add an empty view to story board and connect it to your controller, e.g.:
@IBOutlet weak var gameView: UIView!
Create a subclass of a dynamic behavior and add it with the animator to the game view:
let breakout = BreakoutBehavior() lazy var animator: UIDynamicAnimator = { UIDynamicAnimator(referenceView: self.gameView) }() override func viewDidLoad() { super.viewDidLoad() animator.addBehavior(breakout) }
Add a collision behavior (I will adjust it a little bit later on):
lazy var collider: UICollisionBehavior = { let lazilyCreatedCollider = UICollisionBehavior() return lazilyCreatedCollider }() override init() { super.init() addChildBehavior(collider) }
And a dynamic item behavior for the ball. Its elasticity is 100 %, its friction and resistance zero (Strangely the default should already be zero, but it seems necessary to explicitly set it):
lazy var ballBehavior: UIDynamicItemBehavior = { let lazilyCreatedBallBehavior = UIDynamicItemBehavior() lazilyCreatedBallBehavior.allowsRotation = false lazilyCreatedBallBehavior.elasticity = 1.0 lazilyCreatedBallBehavior.friction = 0.0 lazilyCreatedBallBehavior.resistance = 0.0 return lazilyCreatedBallBehavior }() override init() { ... addChildBehavior(ballBehavior) }
Two methods add and remove a ball to the game view and to its behaviors:
func addBall(ball: UIView) { dynamicAnimator?.referenceView?.addSubview(ball) collider.addItem(ball) ballBehavior.addItem(ball) } func removeBall(ball: UIView) { collider.removeItem(ball) ballBehavior.removeItem(ball) ball.removeFromSuperview() }
From the start there will be no ball, we will add it (or push) it using a tap gesture:
override func viewDidLoad() { ... gameView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "pushBall:")) }
When there is no ball, create one, place it and add it to the the breakout behavior. Afterwards, push it:
func pushBall(gesture: UITapGestureRecognizer) { if gesture.state == .Ended { if breakout.balls.count == 0 { let ball = createBall() placeBall(ball) breakout.addBall(ball) } breakout.pushBall(breakout.balls.last!) } }
To know if there is already a ball, access the items in the collider behavior (or the ball behavior, it does not really matter). Filter out the balls, and map them into a new array:
var balls:[UIView] { get { return collider.items.filter{$0 is UIView}.map{$0 as UIView} } }
A ball is a view with rounded corners (the rest is just cosmetics):
struct Constants { static let BallSize: CGFloat = 40.0 static let BallColor = UIColor.blueColor() } func createBall() -> UIView { let ball = UIView(frame: CGRect(origin: CGPoint.zeroPoint, size: CGSize(width: Constants.BallSize, height: Constants.BallSize))) ball.backgroundColor = Constants.BallColor ball.layer.cornerRadius = Constants.BallSize / 2.0 ball.layer.borderColor = UIColor.blackColor().CGColor ball.layer.borderWidth = 2.0 ball.layer.shadowOffset = CGSize(width: 2.0, height: 2.0) ball.layer.shadowOpacity = 0.5 return ball }
Later on we will place the ball above the paddle, for now place it in the middle of the screen:
func placeBall(ball: UIView) { ball.center = CGPoint(x: gameView.bounds.midX, y: gameView.bounds.midY) }
To push the ball add a instantaneous push behavior with a random angle. Using its action remove the behavior as soon as it has finished (which will be immediately). Don’t forget to pass the push variable weakly, otherwise it will never be released.
func pushBall(ball: UIView) { let push = UIPushBehavior(items: [ball], mode: .Instantaneous) push.magnitude = 1.0 push.angle = CGFloat(Double(arc4random()) * M_PI * 2 / Double(UINT32_MAX)) push.action = { [weak push] in if !push!.active { self.removeChildBehavior(push!) } } addChildBehavior(push) }
Adding the barrier for the top, the left and the right side of the screen, I originally added those three lines as path. But it seems the collision behavior treats paths always as closed. Therefor I now define an “over-sized” rectangle as barrier:
struct Constants { ... static let BoxPathName = "Box" } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() var rect = gameView.bounds rect.size.height *= 2 breakout.addBarrier(UIBezierPath(rect: rect), named: Constants.BoxPathName) }
Defining that barrier when the view did layout, and removing possible previously added barriers with the same name, ensures that even when rotating the device, the bottom of the screen will be open:
func addBarrier(path: UIBezierPath, named name: String) { collider.removeBoundaryWithIdentifier(name) collider.addBoundaryWithIdentifier(name, forPath: path) }
Back to the collision behavior add an action which checks if the ball (or any of them) do at least intersect with the game view. Otherwise, remove them:
lazy var collider: UICollisionBehavior = { ... lazilyCreatedCollider.action = { for ball in self.balls { if !CGRectIntersectsRect(self.dynamicAnimator!.referenceView!.bounds, ball.frame) { self.removeBall(ball) } } } ... }()
Back to rotating the device. Its not nice if the player looses a ball because the device has been rotated accidentally. In such cases put the ball back on screen:
override func viewDidLayoutSubviews() { ... for ball in breakout.balls { if !CGRectContainsRect(gameView.bounds, ball.frame) { placeBall(ball) animator.updateItemUsingCurrentState(ball) } } }
The complete code for step #1 is available on GitHub.