Please note, this blog entry is from a previous course. You might want to check out the current one.
A new structure provides two sets of information – the view of the brick and its relative frame information (without information about the current device information). Store this structure for each brick in a dictionary:
var bricks = [Int:Brick]() struct Brick { var relativeFrame: CGRect var view: UIView }
Place the bricks when ever the layout has changed (e.g. at the start of the app, or when the device gets rotated):
override func viewDidLayoutSubviews() { ... placeBricks() ... } Placing the bricks, just takes the relative frame information boosts it to the device dimensions, and adjusts the barriers for the collision behavior: func placeBricks() { for (index, brick) in bricks { brick.view.frame.origin.x = brick.relativeFrame.origin.x * gameView.bounds.width brick.view.frame.origin.y = brick.relativeFrame.origin.y * gameView.bounds.height brick.view.frame.size.width = brick.relativeFrame.width * gameView.bounds.width brick.view.frame.size.height = brick.relativeFrame.height * gameView.bounds.height brick.view.frame = CGRectInset(brick.view.frame, Constants.BrickSpacing, Constants.BrickSpacing) breakout.addBarrier(UIBezierPath(roundedRect: brick.view.frame, cornerRadius: Constants.BrickCornerRadius), named: index) } }
Because the name of the barriers for the bricks are identical to their index, the method to add barriers needs a tiny adjustment to allow integer values as name parameter:
func addBarrier(path: UIBezierPath, named name: NSCopying) { ... }
With the methods above its just a matter on filling the bricks array to get different layouts.
“Level One” is a simple brick wall with a number of rows and columns. Every row has a different color, nothing too fancy:
struct Constants { ... static let BrickColumns = 10 static let BrickRows = 8 static let BrickTotalWidth: CGFloat = 1.0 static let BrickTotalHeight: CGFloat = 0.3 static let BrickTopSpacing: CGFloat = 0.05 static let BrickSpacing: CGFloat = 5.0 static let BrickCornerRadius: CGFloat = 2.5 static let BrickColors = [UIColor.greenColor(), UIColor.blueColor(), UIColor.redColor(), UIColor.yellowColor()] ... } func levelOne() { if bricks.count > 0 { return } let deltaX = Constants.BrickTotalWidth / CGFloat(Constants.BrickColumns) let deltaY = Constants.BrickTotalHeight / CGFloat(Constants.BrickRows) var frame = CGRect(origin: CGPointZero, size: CGSize(width: deltaX, height: deltaY)) for row in 0..<Constants.BrickRows { for column in 0..<Constants.BrickColumns { frame.origin.x = deltaX * CGFloat(column) frame.origin.y = deltaY * CGFloat(row) + Constants.BrickTopSpacing let brick = UIView(frame: frame) brick.backgroundColor = Constants.BrickColors[row % Constants.BrickColors.count] brick.layer.cornerRadius = Constants.BrickCornerRadius brick.layer.borderColor = UIColor.blackColor().CGColor brick.layer.shadowOffset = CGSize(width: 1.0, height: 1.0) brick.layer.shadowOpacity = 0.1 gameView.addSubview(brick) bricks[row * Constants.BrickColumns + column] = Brick(relativeFrame: frame, view: brick) } } }
Because my game has only one level I define it when the view did load:
override func viewDidLoad() { ... levelOne() }
Now have bricks which adjust their layout for every device orientation … and are already barriers for the ball …
To detect a collision, the view controller will be the collision delegate of the collision behavior. For style create a computed variable to set the delegate via the public API of the breakout behavior:
var collisionDelegate: UICollisionBehaviorDelegate? { get { return collider.collisionDelegate } set { collider.collisionDelegate = newValue } } class ViewController: UIViewController, UICollisionBehaviorDelegate { override func viewDidLoad() { ... breakout.collisionDelegate = self ... } }
When a collision appears and the barrier identifier is an integer (equals a brick), destroy the brick:
func collisionBehavior(behavior: UICollisionBehavior, beganContactForItem item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying, atPoint p: CGPoint) { if let index = identifier as? Int { destroyBrickAtIndex(index) } }
First remove the barrier – even if it. We core animation flip the brick (and make it slightly transparent). Afterwards add it to the behavior, and let it fade out completely. Finally remove the brick from the behavior, the game view and from the brick array:
private func destroyBrickAtIndex(index: Int) { breakout.removeBarrier(index) if let brick = bricks[index] { UIView.transitionWithView(brick.view, duration: 0.2, options: .TransitionFlipFromBottom, animations: { brick.view.alpha = 0.5 }, completion: { (success) -> Void in self.breakout.addBrick(brick.view) UIView.animateWithDuration(1.0, animations: { brick.view.alpha = 0.0 }, completion: { (success) -> Void in self.breakout.removeBrick(brick.view) brick.view.removeFromSuperview() }) }) bricks.removeValueForKey(index) } }
The behavior needs a new method to remove barriers – up to now we only added or reset them:
func removeBarrier(name: NSCopying) { collider.removeBoundaryWithIdentifier(name) }
And when a brick gets hit let them fall down using gravity:
let gravity = UIGravityBehavior() override init() { ... addChildBehavior(gravity) } func addBrick(brick: UIView) { gravity.addItem(brick) } func removeBrick(brick: UIView) { gravity.removeItem(brick) }
That’s it … however … the hints ask for more. Add an additional variable to the brick structure, to hold any possible action you want to happen for each individual brick:
private struct Brick { var relativeFrame: CGRect var view: UIView var action: BrickAction } private typealias BrickAction = ((Int) -> Void)?
Change the collision method to destroy bricks only if no special action for that brick has been defined, otherwise run that action:
func collisionBehavior(behavior: UICollisionBehavior, beganContactForItem item: UIDynamicItem, withBoundaryIdentifier identifier: NSCopying, atPoint p: CGPoint) { if let index = identifier as? Int { if let action = bricks[index]?.action { action(index) } else { destroyBrickAtIndex(index) } } }
As example for such special bricks, I colored the last row black. When a black brick gets hit, it changes its color, and gets destroyed if it gets hit a second time. Because the collision delegate fires multiple times, I had to add a time delay for the color change. Otherwise the brick would change its color and get destroyed in one sweep:
private func levelOne() { ... var action: BrickAction = nil if row + 1 == Constants.BrickRows { brick.backgroundColor = UIColor.blackColor() action = { index in if brick.backgroundColor != UIColor.blackColor() { self.destroyBrickAtIndex(index) } else { NSTimer.scheduledTimerWithTimeInterval(0.05, target: self, selector: "changeBrickColor:", userInfo: brick, repeats: false) } } } bricks[row * Constants.BrickColumns + column] = Brick(relativeFrame: frame, view: brick, action: action) } } } func changeBrickColor(timer: NSTimer) { if let brick = timer.userInfo as? UIView { UIView.animateWithDuration(0.5, animations: { () -> Void in brick.backgroundColor = UIColor.cyanColor() }, completion: nil) } }
… and there is a glitch in my code reseting the paddle, here is the workaround:
private func resetPaddle() { if !CGRectContainsRect(gameView.bounds, paddle.frame) { paddle.center = CGPoint(x: gameView.bounds.midX, y: gameView.bounds.maxY - paddle.bounds.height) } else { paddle.center = CGPoint(x: paddle.center.x, y: gameView.bounds.maxY - paddle.bounds.height) } addPaddleBarrier() }
The complete code for step #3 is available on GitHub.