Difference between revisions of "W1522 Ping Then Pong"

From Coder Merlin
m (Editorial review and minor corrections)
 
(22 intermediate revisions by 5 users not shown)
Line 1: Line 1:
[[File:Signed Pong Cabinet.jpg|thumb|Signed Pong Cabinet]]
[[File:Signed Pong Cabinet.jpg|thumb|Signed Pong cabinet]]
[[File:Pong.png|thumb|Pong]]
[[File:Pong.png|thumb|Pong]]
== Prerequisites ==
== Prerequisites ==
Line 11: Line 11:


== Background ==
== Background ==
As we learned in the previous lab, the {{SwiftIdentifier|render}} event handler is invoked by the system periodically to render objects to the canvas. Another event handler that we haven't used yet is the {{SwiftIdentifier|calculate}} event handler. We'll take advantage of this behavior to move the ball during each cycle, without relying on events produced by the user.
As we learned in the previous lab, the {{SwiftIdentifier|render}} event handler is invoked by the system periodically to render objects to the canvas. Another event handler that we haven't used yet is the {{SwiftIdentifier|calculate}} event handler. We'll take advantage of this behavior to move the ball during each cycle, without relying on events produced by the user.


== Experiment ==
== Experiment ==


=== Getting Started ===
=== Getting Started ===
Continue with the previous project.
Continue from the previous project; we'll be editing all of our files there. Enter into the Sources directory of the project.
 
Enter into the Sources directory of the project.  
{{ConsoleLine|john-williams@codermerlin:|cd ~/Experiences/W1521/Sources/ScenesShell/}}
{{ConsoleLine|john-williams@codermerlin:|cd ~/Experiences/W1521/Sources/ScenesShell/}}
=== Refactoring ===
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.
<syntaxhighlight lang="swift">
  // 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)
    }
</syntaxhighlight>
{{notice|[[File:Oxygen480-actions-help-hint.svg|frameless|30px]]|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.
<syntaxhighlight lang="swift" highlight="4,7">
    override func update(canvas:Canvas) {
        if let canvasSize = canvas.canvasSize {
            calculate(canvasSize:canvasSize)
            if needToDraw {
                paint(canvas:canvas, canvasSize:canvasSize)
                needToDraw = false
            }
        }
    }
</syntaxhighlight>
[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project. <br>View the results in the browser as you did earlier.  Ensure that the application behaves as expected.
=== More Refactoring ===
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:
<syntaxhighlight lang="swift">
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
    }
}
</syntaxhighlight>
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.
{{notice|[[File:Oxygen480-actions-help-hint.svg|frameless|30px]]|Helpful hint:  The syntax <code>Ball.paint()</code> indicates the <code>paint</code> method within the class <code>Ball</code>.}}
Now, make the following changes:
# In "main.swift", change the type of the '''ball''' property from an Ellipse to your new '''Ball''' type.
# Remove the body from your '''paintBall''' method in "main.swift" and move it to the '''Ball.paint()''' method.
# Update your '''Ball.paint()''' method to paint the '''ellipse''' belonging to the ball
# Delete the '''paintBall''' method from "main.swift"
# Update your '''paint()''' method in "main.swift" to invoke '''ball.paint(canvas:canvas)'''
# Update your '''init()''' constructor in "main.swift" to create a new '''Ball''' object.
# Update any references to '''ball.center''' in "main.swift" to invoke the method on the ball, '''ball.move(to:)'''
{{notice|[[File:Oxygen480-actions-help-hint.svg|frameless|30px]]|Helpful hint:  Take your time and ensure that you've completed each one of the above steps before proceeding.}}
[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project. <br>View the results in the browser as you did earlier.  Ensure that the application behaves as expected.


=== A Smarter Ball ===
=== A Smarter Ball ===
Let's teach our ball to move on it's own, given a velocity. Add the following properties to your '''Ball''':
Let's teach our ball to move on its own, given a velocity. Add the following properties to your '''Ball''':
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
     var velocityX : Int
     var velocityX : Int
Line 104: Line 26:
</syntaxhighlight>
</syntaxhighlight>


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


Add another method, '''Ball.changeVelocity()''' to your ball:
Add another method, '''Ball.changeVelocity()''' to your ball:
Line 114: Line 36:
</syntaxhighlight>
</syntaxhighlight>


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)''':
We now need to add some "brains" to our ball so that it "knows" how to move based on its velocity. Add another method, '''Ball.calculate(canvasSize:Size)''':
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
    func calculate(canvasSize:Size) {
    override func calculate(canvasSize: Size) {
         ellipse.center.moveBy(offsetX:velocityX, offsetY:velocityY)
         ellipse.center += Point(x: velocityX, y: velocityY)  
     }
     }
</syntaxhighlight>
</syntaxhighlight>


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'''.
{{RunProgram|Run the program and view in a browser before continuing. Be sure that you understand the role of each method.}}


[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project. <br>View the results in the browser as you did earlier.  Ensure that the application behaves as expected.
=== A Moving Ball ===
Update your {{SwiftIdentifier|'''InteractionLayer'''}} to make the ball an instance variable. This change enables us to easily access the ball later.
<syntaxhighlight lang="swift" highlight="2,9">
class InteractionLayer : Layer {
    let ball = Ball()


=== A Moving Ball ===
    init() {
Update your '''Painter.init()''' constructor by adding a method invocation to change the velocity of the ball:
        // Using a meaningful name can be helpful for debugging
<syntaxhighlight lang="swift">
        super.init(name:"Interaction")
    ball.changeVelocity(velocityX:10, velocityY:5)  
 
        // We insert our RenderableEntities in the constructor
        insert(entity: ball, at: .front)
      }
}
</syntaxhighlight>
</syntaxhighlight>


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):
Now, provide the ball with an initial velocity:
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift" highlight="10">
     ball.calculate(canvasSize:canvasSize)
class InteractionLayer : Layer {
     let ball = Ball()
 
    init() {
        // Using a meaningful name can be helpful for debugging
        super.init(name:"Interaction")
 
        // We insert our RenderableEntities in the constructor
        insert(entity: ball, at: .front)
        ball.changeVelocity(velocityX: 3, velocityY: 5)  
      }
}
</syntaxhighlight>
</syntaxhighlight>


[[File:Start button green arrow.svg|left|link=|Start button green arrow]] Run the project. <br>View the results in the browser as you did earlier. Ensure that the application behaves as expected.
{{RunProgram|Run the program and view in a browser before continuing. Ensure that the application behaves as expected. What happens when you move the mouse on the canvas? Is this what you expected? Why or why not?}}
 
{{notice|[[File:Emblem-question-green.svg|frameless|30px]]|Question:  What happens when you move the mouse on the canvas? Is this what you expected? Why or why not?}}


=== Hit Testing ===
=== Hit Testing ===
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.
The process of determining whether an on-screen, graphical object, such as a ball, intersects with another on-screen, graphical object is called '''hit-testing'''. We'll use hit-testing to determine if our ball intersects with the edge of the canvas. A straightforward (yet sometimes inaccurate) method to perform hit-testing involves drawing an imaginary rectangle around objects of interest and then checking to see whether this '''minimum bounding rectangle''' overlaps the bounding rectangle of any other objects of interest.


Update the '''Ball.calculate(canvasSize:)''' method as follows:
Update the '''Ball.calculate(canvasSize:)''' method as follows:
<syntaxhighlight lang="swift">
<syntaxhighlight lang="swift">
     func calculate(canvasSize:Size) {
     override func calculate(canvasSize:Size) {
         // First, move to the new position
         // First, move to the new position
         ellipse.center.moveBy(offsetX:velocityX, offsetY:velocityY)
         ellipse.center += Point(x:velocityX, y:velocityY)


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


         // Form a bounding rect around the ball (ellipse)
         // Form a bounding rect around the ball (ellipse)
        // After this is working, move the code to your boundingRect() function
         let ballBoundingRect = Rect(topLeft:Point(x:ellipse.center.x-ellipse.radiusX, y:ellipse.center.y-ellipse.radiusY),
         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))
                                     size:Size(width:ellipse.radiusX*2, height:ellipse.radiusY*2))
Line 174: Line 114:


== Exercises ==
== Exercises ==
# Complete the '''Ball.calculate(canvasSize:)''' to properly handle bounces along the y axis
{{W1522-Exercises}}
# 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
* {{MMMAssignment|M1522-28}}
# Deform the ball (squish it) in the direction of the collision for a single frame whenever a collision occurs


== Key Concepts ==
== Key Concepts ==
* '''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.
{{KeyConcepts|
* '''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''
* ''Hit Testing''
* ''Minimum Bounding Rectangle''
* ''Minimum Bounding Rectangle''
}}
[[Category:IGIS]]

Latest revision as of 15:54, 25 May 2022

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 render event handler is invoked by the system periodically to render objects to the canvas. Another event handler that we haven't used yet is the calculate event handler. 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 from the previous project; we'll be editing all of our files there. Enter into the Sources directory of the project.

john-williams@codermerlin: cd ~/Experiences/W1521/Sources/ScenesShell/

A Smarter Ball[edit]

Let's teach our ball to move on its 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 its velocity. Add another method, Ball.calculate(canvasSize:Size):

     override func calculate(canvasSize: Size) {
        ellipse.center += Point(x: velocityX, y: velocityY) 
    }
Start button green arrow
Run the program and view in a browser before continuing. Be sure that you understand the role of each method.

A Moving Ball[edit]

Update your InteractionLayer to make the ball an instance variable. This change enables us to easily access the ball later.

class InteractionLayer : Layer {
    let ball = Ball()

    init() {
        // Using a meaningful name can be helpful for debugging
        super.init(name:"Interaction")

        // We insert our RenderableEntities in the constructor
        insert(entity: ball, at: .front)
      }
}

Now, provide the ball with an initial velocity:

class InteractionLayer : Layer {
    let ball = Ball()

    init() {
        // Using a meaningful name can be helpful for debugging
        super.init(name:"Interaction")

        // We insert our RenderableEntities in the constructor
        insert(entity: ball, at: .front)
        ball.changeVelocity(velocityX: 3, velocityY: 5) 
      }
}
Start button green arrow
Run the program and view in a browser before continuing. Ensure that the application behaves as expected. 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 called hit-testing. We'll use hit-testing to determine if our ball intersects with the edge of the canvas. A straightforward (yet sometimes inaccurate) method to perform hit-testing involves drawing an imaginary rectangle around objects of interest and then checking to see whether this minimum bounding rectangle overlaps the bounding rectangle of any other objects of interest.

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

    override func calculate(canvasSize:Size) {
        // First, move to the new position
        ellipse.center += Point(x:velocityX, y:velocityY)

        // Form a bounding rectangle around the canvas
        let canvasBoundingRect = Rect(size:canvasSize)

        // Form a bounding rect around the ball (ellipse)
        // After this is working, move the code to your boundingRect() function
        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]

ExercisesExercisesIcon.png
  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 over several frames
  3. Whenever a collision occurs, deform the ball (squish it) in the direction of the collision then restore it back to its original dimensions over several frames
  •  M1522-28  Complete  Merlin Mission Manager  Mission M1522-28.

Key Concepts[edit]

Key ConceptsKeyConceptsIcon.png
  • 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