Difference between revisions of "W1522 Ping Then Pong"

From Coder Merlin
Line 26: Line 26:
Let's start by '''refactoring''' our code.  Edit file "main.swift":
Let's start by '''refactoring''' our code.  Edit file "main.swift":
{{ConsoleLine|john-williams@codermerlin:~/Experiences/W1521/Sources/Scenes/Shell$| emacs main.swift}}
{{ConsoleLine|john-williams@codermerlin:~/Experiences/W1521/Sources/Scenes/Shell$| emacs main.swift}}
</syntaxhighlight>


=== Refactoring ===
=== Refactoring ===

Revision as of 13:31, 14 January 2021

Within these castle walls be forged Mavens of Computer Science ...
— Merlin, The Coder
Signed Pong Cabinet
Pong

Prerequisites[edit]

Research[edit]

Background[edit]

As we learned in the previous lab, the update event handler is invoked by the system periodically to refresh the canvas. We'll take advantage of this behavior to move the ball during each cycle, without relying on events produced by the user.

Experiment[edit]

Getting Started[edit]

Continue with the previous project.

Enter into the Sources directory of the project.

cd ~/projects/IgisShell-MovingAlong/Sources/IgisShell/

First Steps[edit]

Let's start by refactoring our code. Edit file "main.swift":

john-williams@codermerlin:~/Experiences/W1521/Sources/Scenes/Shell$  emacs main.swift

Refactoring[edit]

Edit the file by finding the definition of the Painter class's update() method. We'll break apart the functionality into two separate methods. The first method will handle logic and calculation. The second method will handle the actual painting. Begin by creating the following two procedures, copying the code from the update method.

   // This method will perform calculations for our project but will not perform any painting
   func calculate(canvasSize:Size) {
        // Set the coordinates to the center of the screen if they've not yet been set
        if !coordinatesSet {
            let canvasCenter = Point(x:canvasSize.width/2, y:canvasSize.height/2)
            ball.center = canvasCenter
            coordinatesSet = true
        }
    }

    // This method will handle the painting of our objects to the canvas
    func paint(canvas:Canvas, canvasSize:Size) {
        clearScreen(canvas:canvas, canvasSize:canvasSize)
        paintSkyAndGround(canvas:canvas, canvasSize:canvasSize)
        paintBall(canvas:canvas, canvasSize:canvasSize)
    }
Oxygen480-actions-help-hint.svg Helpful hint: The actual code in these methods will vary depending upon your implementation. Rely on the method names to set up your code accordingly. Pay close attention to the ordering of the method invocation. The ordering determines what appears in the foreground and what appears in the background.

Next, we'll invoke the above methods from our update method.

    override func update(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize {

            calculate(canvasSize:canvasSize)

            if needToDraw {
                paint(canvas:canvas, canvasSize:canvasSize)
                needToDraw = false
            }
        }
    }
Start button green arrow

Run the project.
View the results in the browser as you did earlier. Ensure that the application behaves as expected.

More Refactoring[edit]

We'll continue refactoring. Rather than treat the ball as a simple ellipse, we'll create a class for it, in its own file. Split the emacs screen and open a new file named "Ball.swift". Inside this file, place the following text:

class Ball {
    var ellipse : Ellipse

    init(size:Int) {
        ellipse = Ellipse(center:Point(x:0, y:0), radiusX:size, radiusY:size, fillMode:.fillAndStroke)
    }

    func paint(canvas:Canvas) {

    }

    func move(to:Point) {
        ellipse.center = to
    }
}

We've organized the Ball class so that it has a specific size, determined at the time it is created and a method that can ask the ball to paint itself on the canvas.

Oxygen480-actions-help-hint.svg Helpful hint: The syntax Ball.paint() indicates the paint method within the class Ball.


Now, make the following changes:

  1. In "main.swift", change the type of the ball property from an Ellipse to your new Ball type.
  2. Remove the body from your paintBall method in "main.swift" and move it to the Ball.paint() method.
  3. Update your Ball.paint() method to paint the ellipse belonging to the ball
  4. Delete the paintBall method from "main.swift"
  5. Update your paint() method in "main.swift" to invoke ball.paint(canvas:canvas)
  6. Update your init() constructor in "main.swift" to create a new Ball object.
  7. Update any references to ball.center in "main.swift" to invoke the method on the ball, ball.move(to:)
Oxygen480-actions-help-hint.svg Helpful hint: Take your time and ensure that you've completed each one of the above steps before proceeding.
Start button green arrow

Run the project.
View the results in the browser as you did earlier. Ensure that the application behaves as expected.

A Smarter Ball[edit]

Let's teach our ball to move on it's own, given a velocity. Add the following properties to your Ball:

    var velocityX : Int
    var velocityY : Int

In your Ball.init() constructor, initialize both velocityX and velocityY to 0.

Add another method, Ball.changeVelocity() to your ball:

    func changeVelocity(velocityX:Int, velocityY:Int) {
        self.velocityX = velocityX
        self.velocityY = velocityY
    }

We now need to add some "brains" to our ball so that it "knows" how to move based on it's velocity. Add another method, Ball.calculate(canvasSize:Size):

    func calculate(canvasSize:Size) {
        ellipse.center.moveBy(offsetX:velocityX, offsetY:velocityY)
    }

Because we'll be updating the ball's position on every frame, we'll eliminate the needToDraw flag because it will always be true. Edit your "main.swift" file and remove all occurrences of needToDraw.

Start button green arrow

Run the project.
View the results in the browser as you did earlier. Ensure that the application behaves as expected.

A Moving Ball[edit]

Update your Painter.init() constructor by adding a method invocation to change the velocity of the ball:

    ball.changeVelocity(velocityX:10, velocityY:5)

The ball now knows its velocity, but we'll have to invoke the Ball.calculate() method to provide with an opportunity to move. Update your Painter.calculate() method by adding the following (after the conditional):

    ball.calculate(canvasSize:canvasSize)
Start button green arrow

Run the project.
View the results in the browser as you did earlier. Ensure that the application behaves as expected.

Emblem-question-green.svg Question: What happens when you move the mouse on the canvas? Is this what you expected? Why or why not?

Hit Testing[edit]

The process of determining whether an on-screen, graphical object, such as a ball, intersects with another on-screen, graphical object is termed hit-testing. We'll use hit-testing to determine if our ball intersects with the edge of the canvas. A straight-forward (yet sometimes inaccurate) method to perform hit-testing involves drawing an imaginary rectangle around objects of interest and then checking to see whether or not this minimum bounding rectangle overlaps the bounding rectangle of any other objects of interest.

Update the Ball.calculate(canvasSize:) method as follows:

    func calculate(canvasSize:Size) {
        // First, move to the new position
        ellipse.center.moveBy(offsetX:velocityX, offsetY:velocityY)

        // Form a bounding rectangle around the canvas
        let canvasBoundingRect = Rect(topLeft:Point(x:0, y:0), size:canvasSize)

        // Form a bounding rect around the ball (ellipse)
        let ballBoundingRect = Rect(topLeft:Point(x:ellipse.center.x-ellipse.radiusX, y:ellipse.center.y-ellipse.radiusY),
                                    size:Size(width:ellipse.radiusX*2, height:ellipse.radiusY*2))

        // Determine if we've moved outside of the canvas boundary rect
        let tooFarLeft = ballBoundingRect.topLeft.x < canvasBoundingRect.topLeft.x
        let tooFarRight = ballBoundingRect.topLeft.x + ballBoundingRect.size.width > canvasBoundingRect.topLeft.x + canvasBoundingRect.size.width

        let tooFarUp = /***** THIS IS AN EXERCISE LEFT TO THE READER *****/
        let tooFarDown = /***** THIS IS AN EXERCISE LEFT TO THE READER *****/

        // If we're too far to the left or right, we bounce the x velocity
        if tooFarLeft || tooFarRight {
            velocityX = -velocityX
        }

        // If we're too far to the top or bottom, we bound the y velocity
        /***** THIS IS AN EXERCISE LEFT TO THE READER *****/
    }

Exercises[edit]

  1. Complete the Ball.calculate(canvasSize:) to properly handle bounces along the y axis
  2. Implement a power bounce such that immediately after a collision with the canvas edge the ball accelerates to twice its original velocity then slows back to that original velocity
  3. Deform the ball (squish it) in the direction of the collision for a single frame whenever a collision occurs

Key Concepts[edit]

  • Refactoring is a very important process. It's equivalent to rewriting a draft of an English paper to improve it and is an ongoing process for any successful project.
  • Hit Testing
  • Minimum Bounding Rectangle