a simple shoot em up game

swift kick

Here’s a fun programming exercise that you might want to try if you’ve been meaning to learn both Swift and game development for iOS: building a simple “shoot ‘em” game using both!

alienThis game features alien slugs (pictured on the left) that appear at random “altitudes” onscreen and travel from left to right or right to left, right in the line of fire of your ship, located near the bottom of the screen at the center. The player shoots at them by tapping on the screen; the locations of the player’s taps determine the direction of the shots.

As I said, it’s a simple game, but it introduces a fair number of concepts including:Spaceship

  • Drawing, positioning, scaling and moving sprites with Sprite Kit
  • Operator overloading and extensions in Swift
  • Vector math
  • Responding to user taps
  • Figuring out when two sprites collide
  • Playing sound effects and a continuous soundtrack

I thought I’d do things “backwards” with this article series by giving you what you need to complete the project first, and then exploring the code section by section in subsequent articles. That way, you can immediately see what the final result should look like and do some experimenting and exploring on your own.

How to build the simple shoot ‘em up game

  1. Create a new iOS project in Xcode based on the Game template. Give the project any name you like; I named mine SimpleShootEmUp.
  2. Make sure the app uses landscape orientation only by selecting your the project and target in the Project Navigator, and then in the Deployment section, make sure that Portrait is unchecked and Landscape Left and Landscape Right are checked.
  3. Download the .zip file of resources for the game, then unzip it. There’ll be two folders, Images.atlas and Sounds, which you should drag into your project. A dialog box will appear; make sure that Copy items into destination group’s folder (if needed) is checked, and that your project’s target is selected.
  4. And finally, replace the contents of the GameScene.swift file that was automatically generated for your project with the code below. I’ve commented it rather heavily to help you get a better idea of what’s going on:
// GameScene.swift
// for SimpleShootEmUp
// Written by Joey deVilla - August 2014
// using XCode 6 beta 5
// Replace the code that was automatically generated for GameScene.swift
// with this code!
// This code is based on the code from the simple game featured in 
// Ray Wenderlich's article, "Sprite Kit Tutorial for Beginners"
// at RayWenderlich.com:
// (http://www.raywenderlich.com/42699/spritekit-tutorial-for-beginners).

import SpriteKit
import AVFoundation

// MARK: - Vector math operators and CGPoint extensions
// ====================================================
// In this app, we're using CGPoints to do some vector math (yes, there's a CGVector type,
// but in this case, it's just more convenient to use CGPoints to represent both vectors
// and points).
// I've marked these as private to limit the scope of these overloads and extensions
// to this file.

// Vector addition
private func + (left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x + right.x, y: left.y + right.y)

// Vector subtraction
private func -(left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x - right.x, y: left.y - right.y)

// Vector * scalar
private func *(point: CGPoint, factor: CGFloat) -> CGPoint {
  return CGPoint(x: point.x * factor, y:point.y * factor)

private extension CGPoint {
  // Get the length (a.k.a. magnitude) of the vector
  var length: CGFloat { return sqrt(self.x * self.x + self.y * self.y) }
  // Normalize the vector (preserve its direction, but change its magnitude to 1)
  var normalized: CGPoint { return CGPoint(x: self.x / self.length, y: self.y / self.length) }

// MARK: -

class GameScene: SKScene, SKPhysicsContactDelegate {
  // MARK: Properties
  // ================
  // Background music
  // ----------------
  private var backgroundMusicPlayer: AVAudioPlayer!
  // Game time trackers
  // ------------------
  private var lastUpdateTime: CFTimeInterval = 0  // Time when update() was last called
  private var timeSinceLastAlienSpawned: CFTimeInterval  = 0  // Seconds since the last alien was spawned
  // Ship sprite
  // -----------
  // For simplicity's sake, we'll use the spaceship that's provided in Images.xcassets
  // when you start a new Game project
  private let ship = SKSpriteNode(imageNamed: "Spaceship")
  // Physics body category bitmasks
  // ------------------------------
  // We'll use these to determine missle-alien collisions
  private let missileCategory: UInt32 = 0x1 << 0   // 00000000000000000000000000000001 in binary
  private let alienCategory: UInt32   = 0x1 << 1   // 00000000000000000000000000000010 in binary
  // MARK: Events
  // ============
  // Called immediately after the view presents this scene.
  override func didMoveToView(view: SKView) {
    // Start the background music player
    var error: NSError?
    let backgroundMusicURL = NSBundle.mainBundle().URLForResource("background-music", withExtension: "aiff")
    backgroundMusicPlayer = AVAudioPlayer(contentsOfURL: backgroundMusicURL, error: &error)
    backgroundMusicPlayer.numberOfLoops = -1
    // Set the game's background color to white
    backgroundColor = SKColor(red: 1, green: 1, blue: 1, alpha: 1)
    // Position the player's ship halfway across the screen,
    // near the bottom
    ship.position = CGPoint(x: size.width / 2, y: ship.size.height * 1.25)
    // Game physics
    physicsWorld.gravity = CGVector(0, 0) // No gravity in this game...yet!
    physicsWorld.contactDelegate = self // We'll handle contact between physics bodies in this class
    spawnAlien() // Start the game with a single alien
  // Called exactly once per frame as long as the scene is presented in a view
  // and isn't paused
  override func update(currentTime: CFTimeInterval) {
    var timeSinceLastUpdate = currentTime - lastUpdateTime
    lastUpdateTime = currentTime
    if timeSinceLastUpdate > 1 {
      timeSinceLastUpdate = 1.0 / 60.0
      lastUpdateTime = currentTime
  // Called whenever the user touches the screen
  override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) {
    // Select one of the user's touches. Given the event loop's speed, there aren't likely
    // to be more than 1 or 2 touches in the set.
    let touch = touches.anyObject() as UITouch
    let touchLocation = touch.locationInNode(self)
    // Reject any shots that are below the ship, or directly to the right or left
    let targetingVector = touchLocation - ship.position
    if targetingVector.y > 0 {
      // FIRE ZE MISSILES!!!
  // SKPhysicsContactDelegate method: called whenever two physics bodies
  // first contact each other
  func didBeginContact(contact: SKPhysicsContact!) {
    var firstBody: SKPhysicsBody!
    var secondBody: SKPhysicsBody!
    // An SKPhysicsContact object is created when 2 physics bodies make contact,
    // and those bodies are referenced by its bodyA and bodyB properties.
    // We want to sort these bodies by their bitmasks so that it's easier
    // to identify which body belongs to which sprite.
    if contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask {
      firstBody = contact.bodyA
      secondBody = contact.bodyB
    else {
      firstBody = contact.bodyB
      secondBody = contact.bodyA
    // We only care about missile-alien contacts.
    // If the contact is missile-alien, firstBody refers to the missile's physics body,
    // and second body refers to the alien's physics body.
    if (firstBody.categoryBitMask & missileCategory) != 0 &&
      (secondBody.categoryBitMask & alienCategory) != 0 {
        destroyAlien(firstBody.node as SKSpriteNode, alien: secondBody.node as SKSpriteNode)
  // MARK: Game state
  // ================
  func updateWithTimeSinceLastUpdate(timeSinceLastUpdate: CFTimeInterval) {
    // If it's been more than a second since we spawned the last alien,
    // spawn a new one
    timeSinceLastAlienSpawned += timeSinceLastUpdate
    if (timeSinceLastAlienSpawned > 0.5) {
      timeSinceLastAlienSpawned = 0
  func spawnAlien() {
    enum Direction {
      case GoingRight
      case GoingLeft
    var alienDirection: Direction!
    var alienSpriteImage: String!
    // Randomly pick the alien's origin
    if Int(arc4random_uniform(2)) == 0 {
      alienDirection = Direction.GoingRight
      alienSpriteImage = "alien-going-right"
    else {
      alienDirection = Direction.GoingLeft
      alienSpriteImage = "alien-going-left"
    // Create the alien sprite
    let alien = SKSpriteNode(imageNamed: alienSpriteImage)
    // Give the alien sprite a physics body
    alien.physicsBody = SKPhysicsBody(rectangleOfSize: alien.size)
    alien.physicsBody.dynamic = true
    alien.physicsBody.categoryBitMask = alienCategory
    alien.physicsBody.contactTestBitMask = missileCategory
    alien.physicsBody.collisionBitMask = 0
    // Set the alien's initial coordinates
    var alienSpawnX: CGFloat!
    var alienEndX: CGFloat!
    if alienDirection == Direction.GoingRight {
      alienSpawnX = -(alien.size.width / 2)
      alienEndX = frame.size.width + (alien.size.width / 2)
    else {
      alienSpawnX = frame.size.width + (alien.size.width / 2)
      alienEndX = -(alien.size.width / 2)
    let minSpawnY = frame.size.height / 3
    let maxSpawnY = (frame.size.height * 0.9) - alien.size.height / 2
    let spawnYRange = UInt32(maxSpawnY - minSpawnY)
    let alienSpawnY = CGFloat(arc4random_uniform(spawnYRange)) + minSpawnY
    alien.position = CGPoint(x: alienSpawnX, y: alienSpawnY)
    // Put the alien onscreen
    // Set the alien's speed
    let minMoveTime = 2
    let maxMoveTime = 4
    let moveTimeRange = maxMoveTime - minMoveTime
    let moveTime = NSTimeInterval((Int(arc4random_uniform(UInt32(moveTimeRange))) + minMoveTime))
    // Send the alien on its way
    let moveAction = SKAction.moveToX(alienEndX, duration: moveTime)
    let cleanUpAction = SKAction.removeFromParent()
    alien.runAction(SKAction.sequence([moveAction, cleanUpAction]))
  func fireMissile(targetingVector: CGPoint) {
    // Now that we've confirmed that the shot is "legal", FIRE ZE MISSILES!
    // Play shooting sound
    runAction(SKAction.playSoundFileNamed("missile.mp3", waitForCompletion: false))
    // Create the missile sprite at the ship's location
    let missile = SKSpriteNode(imageNamed: "missile")
    missile.position.x = ship.position.x
    missile.position.y = ship.position.y + (ship.size.height / 2)
    // Give the missile sprite a physics body
    missile.physicsBody = SKPhysicsBody(circleOfRadius: missile.size.width / 2)
    missile.physicsBody.dynamic = true
    missile.physicsBody.categoryBitMask = missileCategory
    missile.physicsBody.contactTestBitMask  = alienCategory
    missile.physicsBody.collisionBitMask = 0
    missile.physicsBody.usesPreciseCollisionDetection = true
    // Calculate the missile's speed and final destination
    let direction = targetingVector.normalized
    let missileVector = direction * 1000
    let missileEndPos = missileVector + missile.position
    let missileSpeed: CGFloat = 500
    let missileMoveTime = size.width / missileSpeed
    // Send the missile on its way
    let actionMove = SKAction.moveTo(missileEndPos, duration: NSTimeInterval(missileMoveTime))
    let actionMoveDone = SKAction.removeFromParent()
    missile.runAction(SKAction.sequence([actionMove, actionMoveDone]))
  func destroyAlien(missile: SKSpriteNode, alien: SKSpriteNode) {
    // Play explosion sound
    runAction(SKAction.playSoundFileNamed("explosion.wav", waitForCompletion: false))
    // When a missile hits an alien, both disappear

Once that’s done, the game’s ready to run. Go ahead and run it, play, explore the code, experiment, and learn! In the next installment, I’ll start explaining the code, starting with some Sprite Kit basics.

Credit where credit is due

  • Much of the code is a Swift adaptation of the code in Ray Wenderlich’s September 2013 article, Sprite Kit Tutorial for Beginners. If you’re serious about learning iOS development, you want to bookmark Ray’s site, RayWenderlich.com, the home of not just tutorial articles, but tutorial videos, very in-depth books, and even starter kits for budding iOS game developers.
  • The alien images were taken from Backyard Ninja Design’s Free Stuff page, specifically the set of sprite from their “Crapmunch” game.
  • The ship image is the one included in the example game that’s automatically created when you generate a game project.
  • The background music is FU4, created by Partners in Rhyme, and can be found on their Free Royalty-Free Music Loops page.

{ 1 comment }

hundred dollar bills

Creative Commons photo by “2bgr8″. Click to see the original.

With wireless carriers fighting tooth-and-nail for each other’s business, the growing usage of mobile data, and new technologies and service driving increased mobile usage, wireless has become a dynamic cost center. It’s also an extraordinarily difficult expense for businesses to control.

Vendor rate plans change with the wind, users are constantly adding or changing phones and device accessories, and what was initially an optimal plan for an individual or group of employees may no longer fit when usage patterns change. What’s more, errors are notoriously rampant in wireless billing, and even when rectified, they have a way of creeping back into invoices.

The majority of wireless expense management companies take a cut-and-dried approach to cost reduction. This generally involves a single invoice audit and optimization of a company’s wireless service plans, features and usage. When all goes smoothly, this process may produce an immediate savings of 10% – 15% of the company’s total spend. After that, the expense management firm bids their client “adieu” and pats itself on the back for a job well done.

dirty dishes

Public domain photo by “Mysid”. Click to see the original.

Just as it would be nice if you could wash the dishes only once, it would be great if wireless expense reduction could be addressed with a single invoice audit. As with the dishes, the reality is that cost reduction is something best done on a continual basis, while making adjustments as your situation changes. Up-front savings are nice, but the real payoff comes from an ongoing audit and optimization process that’s tightly synchronized with the evolution of your business.

Our president, Dan Hughes, has written a white paper that provides an overview of audit and optimization “best practices” that have proven to drive significant and long-term wireless expense reduction. Download and read it, and if your audit firm or wireless expense management firm isn’t following these practices, you’re undoubtedly missing out on the substantial savings that can come from analyzing your wireless spending.

download pdf

Download our white paper [2.1MB PDF] to learn about the audit checks, optimizations, and implementations that we perfom to help our customers maximize their telecom dollar.

this article also appears in the GSG blog


DOT likely to ban in-flight phone calls

mobile phone on flight

The Wall Street Journal reports that the U.S. Department of Transportation (DOT) will likely rule to ban in-flight mobile phone calls. Regulators are focused on the disruptions caused by voice calls and not on texting or data usage. Airlines say that the DOT is overstepping its authority, and would rather that the final decision be left up to them.

In case you were wondering, Delta and JetBlue have gone on the record saying that they won’t allow in-flight calls, and public opinion is generally against them:

public opinion on in-flight calls

Click the graph to see the data source.

Using your unlimited data plan in an unlimited fashion? Carriers don’t like that.

wireless data

Adapted from Intel Free Press’ Creative Commons photo. Click to see the original.

Last week, Tom Wheeler, the Federal Communications Commission’s (FCC) chairman sent a letter to Verizon about their proposed “network optimization” plan asking why only certain customers are targeted for throttling — that is, the slowing down of data connectivity — when accessing “particular cell sites experiencing unusually high demand”. The targeted customers are heavy data users with who have unlimited data plans — in other words, people who use a lot of mobile data and accordingly purchased a plan that meets their needs. ReadWrite reports that he accused the carrier of using “network management” as a money grab:

Reasonable network management concerns the technical management of your network; it is not a loophole designed to enhance your revenue streams. It is disturbing to me that Verizon Wireless would base its network management on distinctions among its customers’ data plans, rather than on network architecture or technology.

Verizon’s response has been to call Wheeler’s letter “incorrect” and “surprising”, and to point out that the FCC hasn’t made any complaints about similar practices from the other carriers. As ReadWrite puts it, “It’s a polished and professional way of saying, Everyone else is doing it too, so what gives?

Why is this the case? The chart below, which we published in an earlier article, explains it quite simply:

more than half is from data

Click the graph to see it at full size.

In the pre-smartphone era, when the primary use for a mobile phone was talking, voice was metered and data (mostly for texting) as unlimited. Over the past few years, now that “mobile phone” means “smartphone” and data usage has skyrocketed, the carriers have been adjusting their business models accordingly, giving away voice and metering data:

The message from the carriers is clear: If you have an unlimited plan and you’re using it for what you think is its intended purpose, prepare to be throttled.

Comcast sweetens their internet service offering for low-income customers after complaints

comcast internet essentials

Comcast’s Internet Essentials program, which offers home internet service to low-income households at $10/month, was a concession made to help them secure the approval of their purchase of NBCUniversal in 2011. It sounds nice, but as the California Emerging Technology Fund (CETF) has complained that signing up for the service is deliberately long, often taking two to three months.

In response, Comcast have announced that it will offer “up to six months” of free internet and amnesty to people who are late paying their bills. Chances are that they’re doing this in light of their recent embarrassment with that cancellation call recording that went viral and needing to gain approval for merging with Time Warner.

this article also appears in the GSG blog


Internet protocol joke of the day

by Joey deVilla on August 5, 2014

i don't always tell udp jokes

Thanks to Joe Smith for pointing me to the original, which I spruced up.


stealth mode activated

Seriously, the right to be forgotten is a silly, unenforceable concept.


legal books

Creative Commons image by Thomas Eagle. Click the photo to see the source.

A preeminent U.S. law firm with over 1,000 employees and an inventory of over 2,000 mobile devices was facing a number of costly problems:

  • Hundreds of zero-use devices that were still being paid for
  • Unchecked fees arising from overages
  • Mixing of personal and business mobile expenses leading to accounting woes
  • Skyrocketing mobile costs that weren’t being measured or managed

The firm needed to bring their wireless devices and spending under control, and did so with GSG’s help. Using GSGCloud, we were able to perform an audit of their mobile assets, services, and costs, and set into motion a plan to bring them under control.

download pdf

Download our case study [1.1MB PDF] to find out how we brought them an annualized savings of $511,000 and complete visibility into their wireless spending and allocation of costs.

this article also appears in the GSG blog


evil monkey pointing at swift code

What happens when you mark operator overload definitions as private?

swift kickWhile I was translating the code in the RayWenderlich.com article Sprite Kit Tutorial for Beginners from its original Objective-C to Swift, I got to the point where he provides some routines to do vector math on CGPoints. These are “standalone” utility functions and aren’t methods inside any class.

The first three functions are for vector arithmetic:

  • Adding two vectors together: (x1, y1) + (x2, y2) = (x1 + x2, y1 + y2)
  • Subtracting on vector from another: (x1, y1) - (x2, y2) = (x1 - x2, y1 - y2)
  • Multiplying a vector by a scalar: (x, y) * k = (k * x, k * y)

Here’s his original code, written in Objective-C:

// Objective-C

static inline CGPoint rwAdd(CGPoint a, CGPoint b) {
    return CGPointMake(a.x + b.x, a.y + b.y);
static inline CGPoint rwSub(CGPoint a, CGPoint b) {
    return CGPointMake(a.x - b.x, a.y - b.y);

static inline CGPoint rwMult(CGPoint a, float b) {
    return CGPointMake(a.x * b, a.y * b);

I’ve always found methods like add(firstThing, secondThing) a little clunky-looking; I’d much rather write it as firstThing + secondThing. Since Swift supports operator overloading (Objective-C doesn’t), I decided to implemented the rwAdd(), rwSub(), and rwMult() functions as overloads of the +, -, and * operators. Here’s my equivalent Swift code:

// Swift

// Vector addition
private func + (left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x + right.x, y: left.y + right.y)

// Vector subtraction
private func - (left: CGPoint, right: CGPoint) -> CGPoint {
  return CGPoint(x: left.x - right.x, y: left.y - right.y)

// Vector * scalar
private func * (point: CGPoint, factor: CGFloat) -> CGPoint {
  return CGPoint(x: point.x * factor, y:point.y * factor)

Note that I added a private access modifier to each of the functions above. As I pointed out in my last Swift article, Swift’s access modifiers are based on files and modules, not the class hierarchy. In Swift, any entity marked private is visible and accessible within its own file, and invisible and inaccessible from outside its own file. I wanted to see what would happen to operator overloads.

Here’s what I found:

Operator overloads marked private are available within the file where they’re defined, and are not available outside that file. Outside the file where the private operator overloads were defined, any attempt to use them results in an error.

A little testing confirmed that I could add and subtract CGPoints and multiply them by scalars using the + operation from inside the same file, but couldn’t do so from inside in other files.

What happens when you mark extensions as private?

The next couple of functions that were in Sprite Kit Tutorial for Beginners were for getting and normalizing the length of a vector. Here’s the original Objective-C code:

// Objective-C

static inline float rwLength(CGPoint a) {
    return sqrtf(a.x * a.x + a.y * a.y);
// Makes a vector have a length of 1
static inline CGPoint rwNormalize(CGPoint a) {
    float length = rwLength(a);
    return CGPointMake(a.x / length, a.y / length);

Swift supports the addition of functionality to types by means of extensions. I thought that implementing these functions as computed properties of CGPoint in an extension would make for more elegant code — I’d rather write vector.length and vector.normalized than rwLength(vector) and rwNormalize(vector). Here’s what I wrote:

// Swift

private extension CGPoint {
  // Get the length (a.k.a. magnitude) of the vector
  var length: CGFloat { return sqrt(self.x * self.x + self.y * self.y) }
  // Normalize the vector (preserve its direction, but change its magnitude to 1)
  var normalized: CGPoint { return CGPoint(x: self.x / self.length, y: self.y / self.length) }

Note that I marked the entire extension as private. How would that affect their visibility inside and outside the file where the extension was defined?

The members of extensions marked private are available within the file where they’re defined, and are not available outside that file. Outside the file where the private extension members were defined, any attempt to use them results in an error, and auto-complete wouldn’t even list them.

Once again, testing confirmed that I could normalize and get the length of vectors represented by CGPoints from inside the same file, but couldn’t do so from inside in other files.

What happens when you mark “monkeypatches” as private?

Marking overrides and extensions as private led me to wonder what would happen if I marked a “monkeypatch” as private.

Monkeypatching is a term that’s used in the Python and Ruby developer communities, and it means dynamically replacing an existing class method with one of your own. It’s takes some effort to do in Objective-C (thanks to Joe Smith for the heads-up!), but it’s quite simple to do in Swift — and not just to methods, but properties as well. Here’s a quick example, in which I redefine the String class property utf16Count, which returns the number of UTF-16 characters in a string (it maps to NSString‘s length method):

extension String {
  var utf16Count: Int { return 5 }

With this extension, I’ve monkeypatched utf16Count so that it always returns 5, no matter what the number of UTF-16 characters in the string is.

Monkeypatching is powerful, and it’s sometimes useful, but as smarter people than I have pointed out, it can create more problems than it solves. As Jeff Atwood asked in Coding Horror, “Can you imagine debugging code where the String class had subtly different behaviors from the Stringyou’ve learned to use?”

That’s what got me thinking: what if private could be used to limit the scope of a monkeypatch, to limit the applicability of my warped verstion of utf16Count to a single file? Is such a thing possible? To answer these questions, I created this extension to String in one file:

// File: ViewController.swift

private extension String {
  var utf16Count: Int { return 5 }  // already defined in String
  var someNumber: Int { return 10 } // a new addition to String

This extension provides two things:

  • A monkeypatch for String‘s already-existent utf16Count property so that it always returns the value 5
  • A new property for String called someNumber, which always returns the value 10

In the same file where I defined my extension to String, I wrote this code:

// File: ViewController.swift

let testString = "This is a test"
println("UTF-16 count inside: \(testString.utf16Count)")

And in another file, I defined the doOutsideCount method:

// File: SomeOtherFile.swift

func doOutsideCount() {
  let testString = "This is a test"
  println("UTF-16 count outside: \(testString.utf16Count)")

If making the extension private limited my redefinition of utf16Count to the file where I redefined it, the output of doOutsideCount() would be “UTF-16 count outside: 14″, since there are 14 UTF-16 characters in testString. I noticed that while typing in the code in this file, utf16Count was available to me in auto-complete.

Here’s the output that appeared on the console when I ran the app:

UTF-16 count inside: 5
UTF-16 count outside: 5

It appears that monkeypatching (redefining an existing member) in an extension marked private does not limit the monkeypatch to the file where the monkeypatch was defined. You can’t use private to limit the scope of a monkeypatch to a single file.

I changed the code in both files to try calling on someNumber, a method that didn’t already exist in String:

// File: ViewController.swift

func doOutsideCount() {
  let testString = "This is a test"
  println("someNumber inside: \(testString.someNumber)")

// File: SomeOtherFile.swift

func doOutsideCount() {
  let testString = "This is a test"
  println("someNumber inside: \(testString.someNumber)")

This wouldn’t even compile. Calling on the someNumber property of String outside of the file where I defined the private extension raises an error: 'String' does not have a member named 'SomeNumber'.

My conclusion from this little bit of experimenting is also this article’s title:

You can use Swift’s private access modifier to limit the reach of overrides and extensions, but not monkeypatches.

{ 1 comment }