Skip to main content

Physics and Scripting for Ping Pong Minigame

This tutorial explains the Physics and Scripting setup required to make a minigame

tip

Watch the video tutorials or follow our step-by-step written tutorial with examples.

Download files before you begin the tutorial

For simplicity, we will start with the static objects already prepared in a project.

Download files

Physics and Scripting Part 1

Physics and Scripting Part 2

Physics and Scripting Part 3

Open the DeepAR Studio Project

To follow along with the video tutorial, find the paddle_basic.deeparproj project in the downloads and open it. Start the live preview to see the effect applied to your head.

Set up Physics

We will make the Ball fall, and collide with the Paddle. If you want more information on what specific components are, do check out the Physics Mechanics docs.

Add Physics World

The first step of setting up Physics is to add a Physics World component to your effect. This component needs to be on a parent node to all Physic Body components. Select the Root Node and add a Physics World component.

add-physics-world

Enable the Physics collider in the Preview

Now it would be a good time to enable colliders in the preview so you can better understand how the rest of the components will behave.

Enable the Physics collider

Set up Physics Bodies and Colliders

Select the Ball Node in the hierarchy and add the Physics Sphere Body component.

Set up Physics Bodies and Colliders

You will see that the ball now falls but this collider won't be visible in the Preview yet because it's smaller than the ball. To fix this you will need to increase the Radius to at least 2 so it matches the size of the ball.

Set up Physics Bodies and Colliders2

The collider will now be visible.

You will also want to make the ball faster and bouncier. To achieve this, increase the Restitution and Mass parameters as shown below.

Finally, to avoid the ball moving forward and back we can fix it's Z position. This will be useful when we bounce the ball.

Set up Physics Bodies and Colliders3

Next, we will want to add a collider to the paddle as well. To do this, we recommend you create a new Node, named PaddleCollider.

Set up Physics Bodies and Colliders4

Let's position this node so it's aligned with the paddle.

Set up Physics Bodies and Colliders5

We can approximate the shape of the paddle with a box, so let's add a Physics Box body to the PaddleCollider Node.

Check the Static Body box to fix it in place and prevent it from falling as the ball does. It will still move relative to the face.

Next increase the Restitution and the Extent so that it covers the entire paddle.

Set up Physics Bodies and Colliders6

Adjust the Physics World

The ball will likely still be a bit slow and not bouncy enough, but we can adjust this by changing the Physics world properties. Select the Root Node again to edit them.

We want to increase the Kinematic knockback factor as well as the gravity.

Adjust-the-Physics-World

Reset Colliders after a face is lost

Due to your static object (paddle) being tied to the face position, it might behave incorrectly after a face is lost.

To avoid this, select a parent object of the Paddle Collider and add a Disable Child Nodes component. Set the Disable Condition to Face invisible.

Reset-colliders-after-a-face-is-lost

You have successfully finished the basic Physics set up! 🎉

The next thing we need to do is create a script to control the game behavior.

Script

We will make a script that reset the ball when it's lost and reacts to successful bounces.

For more info about scripting, check out the Scripting Documentation.

Add the Script

First, we need to create the script. To do this click the + button int the Assets panel and select the Script File.

Add-the-Script

Rename the script so that it has a meaningful name, such as PingPong.js and save your project. You can now open the script in a text editor of your choice.

The generated script file will look like this:

// Use this for initialization
function onStart() {


}


// Update is called once per frame
function onUpdate() {


}

OnStart will be called as soon as the script initializes, and onUpdate will be called for every frame.

Whenever you are ready to apply your script, you should add it to a Node. Add the created Script to the Root Node.

Add-the-Script2

Find the Ball

We will need to let our script know where to find the ball so we will create a variable for the ball and assign its value in the onStart() function. We have also added a 'start' log to verify that the script loaded correctly.

var ball;


// Use this for initialization
function onStart() {
Debug.log('start');
ball = Node.root.getChild('Ball');
}

If you save and reload, you should see a Scripting console pop up with the log:

Find-the-Ball

Face Visibility

As mentioned in Reset colliders after a face is lost, there is a component that will enable and disable nodes depending on the visibility of the face. Previously you were instructed to place it on a parent of the Paddle Collider, for this part of the tutorial you will want to place it on a parent of both the Ball and the paddle, so if it wasn't already on the root node, place it there.

We still need to reset the ball position when the face reappears. That is if the face is visible in this frame but it was not visible in the previous frame. For that, we will need to remember if the face was visible in the last frame and add a resetBall() function.

...
var prevFaceVisible;


function resetBall(){
ball.localPosition = new Vec3(0, 8, 15);
}


// Use this for initialization
function onStart() {
...
prevFaceVisible = Context.isFaceVisible();
if (prevFaceVisible){
resetBall();
}
}


// Update is called once per frame
function onUpdate() {
var faceVisible = Context.isFaceVisible();
if (faceVisible){
if (!prevFaceVisible){
resetBall();
}
}
prevFaceVisible = faceVisible;
}

The position used in the resetBall() function was found by reading the start position of the ball in the Studio and adjusting it to be centered above the paddle.

Face-Visibility

Dropped the Ball?

Similarly, there are many ways to check if the ball has fallen out of the playable area. The easiest is to temporarily disable physics and simply drag the ball object down until it becomes invisible in the Preview. Then use that local position as a condition to reset the ball position.

// Update is called once per frame
function onUpdate() {
...
if (ball.localPosition.y < -25){
resetBall();
}
...
}

When Nodes Collide

Now let's make the effect a bit more interactive.

We'd like to indicate when the ball bounces off the paddle successfully.

If you look at the white_rubber_mat material, you will see that the Color of the paddle rubber can be easily changed. Let's first change this color when the ball bounces.

When-Nodes-Collide

Let's go back to the script, there's still some work to do.

First, we need two new variables. One for the paddleCollider and one to remember the color state. We will initialize them OnStart, similar to the Ball and prevFaceVisible.

Now we can use the onCollisionEnter callback function to check if the objects collided are the ball and the paddle (technically, those are the only two objects that could collide in this effect, but let's do it anyway for good practice).

For this, we have the checkIfCollidedWithPaddle function.

If our collision is between the Ball and the Paddle we can use Utility.changeParameter to change the paddle color, and toggle the value of our colorToggle variable.

// Use this for initialization
function onStart() {
...
paddleCollider = Node.root.getChild('paddle_v4.fbx').getChild('paddle').getChild('PaddleCollider');
colorToggle = true;
...
}


function onCollisionEnter(firstNode, secondNode){
if (!checkIfCollidedWithPaddle(firstNode, secondNode)) return;
changePaddleColor();
}


function checkIfCollidedWithPaddle(firstNode, secondNode) {
var collides = false;
if (firstNode.equals(ball))
collides = secondNode.equals(paddleCollider);
else if (firstNode.equals(paddleCollider))
collides = secondNode.equals(ball);
return collides;
}


function changePaddleColor() {
if (colorToggle)
Utility.changeParameter('white_rubber', 'MeshRenderer',
'u_color', 0.6, 0.1, 0.00, 1.0)
else
Utility.changeParameter('white_rubber', 'MeshRenderer',
'u_color', 0.1, 0.6, 0.0, 1.0);

colorToggle = colorToggle ? false : true;
}

Sound

Let's add a sound effect for when the ping-pong ball hits the paddle, as is often heard in real-life games. We can leave the script alone for a moment and visit the Assets panel and the Animation Controller.

Import the hit.mp3 sound from the Assets panel.

Sound

Select the Root node, and open the Animation Builder. Window Animation Builder

Animation-Builder

Create an empty animation and add a new State. This new state will play the hit sound. The triggers are shown below:

Initial State [custom trigger 'hit'] State Initial [OnEnd]

Animation-Builder2

Now all we need to do is send the 'hit' trigger from our script.

function onCollisionEnter(firstNode, secondNode){
if (!checkIfCollidedWithPaddle(firstNode, secondNode)) return;
...
Utility.fireTrigger('hit');
}

The sound should now play.

Try it out

Test your effect in the preview and compare your script to ours.

var ball;
var prevFaceVisible;
var paddleCollider;
var colorToggle;


function resetBall(){
ball.localPosition = new Vec3(0, 8, 15);
}


// Use this for initialization
function onStart() {
Debug.log('start');
ball = Node.root.getChild('Ball');
paddleCollider = Node.root.getChild('paddle_v4.fbx').getChild('paddle').getChild('PaddleCollider');
colorToggle = true;
prevFaceVisible = Context.isFaceVisible();
if (prevFaceVisible){
resetBall();
}
}


// Update is called once per frame
function onUpdate() {
var faceVisible = Context.isFaceVisible();
if (faceVisible){
if (!prevFaceVisible){
resetBall();
}
}
if (ball.localPosition.y < -25){
resetBall();
}
prevFaceVisible = faceVisible;
}


function onCollisionEnter(firstNode, secondNode){
if (!checkIfCollidedWithPaddle(firstNode, secondNode)) return;
changePaddleColor();
Utility.fireTrigger('hit');
}


function checkIfCollidedWithPaddle(firstNode, secondNode) {
var collides = false;
if (firstNode.equals(ball))
collides = secondNode.equals(paddleCollider);
else if (firstNode.equals(paddleCollider))
collides = secondNode.equals(ball);
return collides;
}


function changePaddleColor() {
if (colorToggle)
Utility.changeParameter('white_rubber', 'MeshRenderer',
'u_color', 0.6, 0.1, 0.00, 1.0)
else
Utility.changeParameter('white_rubber', 'MeshRenderer',
'u_color', 0.1, 0.6, 0.0, 1.0);

colorToggle = colorToggle ? false : true;
}