The possibilities of APL have grown steadily since its release in 2018. With version 1.4, APL is now so powerful that even highly interactive games can be implemented. Today, I will demonstrate how to implement a Pong style game.
To implement the game in APL, we need the following components:
1) a playing field
2) the interface
3) the "physics" of the game
Preparation
Before we get started, we first define some resources. Resources have the advantage that they can be referenced by different components in the document. For example, we need the width and height of the paddle or the initial position of the ball at different points during the implementation and we will make this consistent using resources.
Resources are divided into blocks - with APL version 1.4 these blocks have been expanded by numbers, booleans, gradients and easing functions. For our purposes, however, we will focus on the numbers and strings block.
The structure of the resources is kept very simple and all we need to do is provide a name and assign a value (key-value pair):
"resources": [
{
"numbers": {
"matchFieldWidth": "${viewport.width}",
"matchFieldHeight": "${viewport.height}",
"paddleWidth": 10,
"paddleHeight": 100,
"ballRadius": 10,
"wallHeight": 4,
"netWidth": 4,
"netHeightOutter": 30,
"netHeightInner": 10,
"spacingToEdge": 25,
"playerPaddlePositionX": "@spacingToEdge",
"cpuPaddlePositionX": "${@matchFieldWidth - @paddleWidth - @spacingToEdge}",
"initialPlayerPaddlePositionY": "${(@matchFieldHeight - @paddleHeight) / 2}",
"initialCpuPaddlePositionY": "${(@matchFieldHeight - @paddleHeight) / 2}",
"initialCpuPaddleSpeed": 8,
"initialBallVelocityX": 8,
"initialBallVelocityY": 8,
"initialBallSpeed": 10,
"initialBallPositionX": "${@matchFieldWidth / 2}",
"initialBallPositionY": "${@matchFieldHeight / 2}"
},
"strings": {
"ballDirectionLeft": "LEFT",
"ballDirectionRight": "RIGHT",
"initialBallDirection": "@ballDirectionRight"
}
}
],
As you can see, a value does not always have to be defined statically: We can use "expression evaluation" to refer to already defined values within the resources or to the variables within the initial context and calculate a value dynamically at runtime.
We have now defined our dimensions, initial positions and other helpful resources, it's time for the next step.
The playing field
For our playing field we will use a vector graphic. Vector graphics have the advantage that they can easily be scaled up or down, and they also support the gesture handlers introduced in APL 1.4.
Alexa Vector Graphics, (short AVG) are standalone documents embedded within an APL document. The structure is very similar to the structure of an APL document. An AVG is defined in the APL document under the name "graphics".
We will name our graphic "Pong".
"graphics": {
"Pong": {
"type": "AVG",
"version": "1.1",
"width": "@matchFieldWidth",
"height": "@matchFieldHeight",
"items": []
}
},
This is the minimal definition of an AVG document.
We use the defined resources "matchFieldWidth" and "matchFieldHeight" for the height and width of the graphic. To reference a resource, we put the @ sign in front of the name.
To display our game field on the screen we need a VectorGraphic component. The value of the source attribute corresponds to the name of the graphic.
The mainTemplate should look like this:
"mainTemplate": {
"parameters": [
"payload"
],
"items": [{
"type": "VectorGraphic",
"source": "Pong",
"width": "@matchFieldWidth",
"height": "@matchFieldHeight"
}]
}
It's time to fill the playing field with life. We will now define the top and bottom walls, the ball and paddles:
"graphics": {
"Pong": {
"type": "AVG",
"version": "1.1",
"width": "@matchFieldWidth",
"height": "@matchFieldHeight",
"items": [
{
"description": "top Wall",
"type": "path",
"fill": "#ffffff",
"pathData": "M0,0 L${@matchFieldWidth},0 L${@matchFieldWidth},${@wallHeight} L0,${@wallHeight} Z"
},
{
"type": "group",
"description": "bottom Wall",
"translateY": "${@matchFieldHeight - @wallHeight}",
"items": [
{
"type": "path",
"fill": "#ffffff",
"pathData": "M0,0 L${@matchFieldWidth},0 L${@matchFieldWidth},${@wallHeight} L0,${@wallHeight} Z"
}
]
},
{
"type": "group",
"description": "User paddle",
"translateX": "@playerPaddlePositionX",
"translateY": "@initialPlayerPaddlePositionY",
"items": [
{
"type": "path",
"fill": "#ffffff",
"pathData": "M0,0 L${@paddleWidth},0 L${@paddleWidth},${@paddleHeight} L0,${@paddleHeight} Z"
}
]
},
{
"type": "group",
"description": "CPU paddle",
"translateX": "@cpuPaddlePositionX",
"translateY": "@initialCpuPaddlePositionY",
"items": [
{
"type": "path",
"fill": "#ffffff",
"pathData": "M0,0 L${@paddleWidth},0 L${@paddleWidth},${@paddleHeight} L0,${@paddleHeight} Z"
}
]
},
{
"type": "group",
"description": "Ball",
"translateX": "@initialBallPositionX",
"translateY": "@initialBallPositionY",
"items": [
{
"type": "path",
"fill": "#ffffff",
"pathData": "M-${@ballRadius},0a${@ballRadius},${@ballRadius} 0 1,0 ${@ballRadius * 2},0a${@ballRadius},${@ballRadius} 0 1,0 -${@ballRadius * 2},0"
}
]
}
]
}
},
To be able to position a path within a vector graphic, it is necessary to enclose it with an AVG group item. As you can see, we do not use static paths, but create them dynamically at runtime using the defined resources.
There are still two elements missing, the score and the net. For the missing items we will use two new APL 1.4 features, the AVG text item and Patterns.
Let's start with our net: a naive approach would be to implement the net as a series of paths with a small distance between them. The disadvantage would be that we would have to do this for each possible viewport height separately! The new patterns in APL 1.4 offer an elegant alternative: patterns allow us to define fragments that can be reused - for example to fill another path. As mentioned above, AVGs are standalone documents and allow to define resources in the same way as the APL document. We are going to add a resources block with a pattern definition to our AVG.
"graphics": {
"Pong": {
"type": "AVG",
"resources": [
{
"patterns": {
"Net": {
"width": "@netWidth",
"height": "@netHeightOutter",
"items": [
{
"type": "path",
"fill": "#ffffff",
"pathData": "M0,0 L${@netWidth},0 L${@netWidth},${@netHeightInner} L0,${@netHeightInner} Z"
}
]
}
}
}
],
"version": "1.1",
The definition specifies a pattern that is 2dp wide and 30dp high, and a path that is also 2dp wide but only 10dp high.
We will now position another group for our net with a path in the middle of the screen next to our paddles and the ball.
{
"type": "group",
"description": "Net",
"translateX": "${@matchFieldWidth / 2}",
"items": [
{
"type": "path",
"fill": "@Net",
"pathData": "M0,0 L2,0 L2,${@matchFieldHeight} L0,${@matchFieldHeight} Z"
}
]
},
We add the net as the first element in our items array to avoid overlapping. The structure of the group is similar to the already existing elements. The difference is that we use our pattern to fill in the path.
The last element missing is the current score display, for this we use the next feature from the APL 1.4 update - AVG text item.
{
"type": "text",
"description": "Player score",
"fill": "#ffffffd9",
"fontSize": 100,
"fontWeight": "700",
"text": "0",
"x": "${@matchFieldWidth / 4}",
"y": "${@matchFieldHeight / 5}"
},
{
"type": "text",
"description": "CPU score",
"fill": "#ffffffd9",
"fontSize": 100,
"fontWeight": "700",
"text": "0",
"textAnchor": "middle",
"x": "${3 * @matchFieldWidth / 4}",
"y": "${@matchFieldHeight / 5}"
}
We do not need a surrounding group for the text elements since we can use the x and y property to position the items directly. For simplicity's sake, we won't define additional resources for the positions as we only need them here. We use textAnchor middle to center the text on our x and y coordinate. Without this parameter, the text might pop out of the screen in case of a larger number.
We have now created all the elements for our field, it's time to bring some action into the game.
The Interface
To be able to move the paddle of the player we will use the gesture handlers introduced with APL 1.4. Currently the handlers only work with a VectorGraphic or TouchWrapper component. For our case we need the onMove handler, but additionally we have to define the onUp and onDown handler. In the next step we add the necessary attributes to our VectorGraphic component.
{
"type": "VectorGraphic",
"source": "Pong",
"width": "${@matchFieldWidth}",
"height": "${@matchFieldHeight}",
"onDown": [],
"onUp": [],
"onMove": []
}
Within the handlers we have access to detailed information such as the x and y coordinates or the dimensions of the component that triggered the event. Before we can continue with our handler, we have to define a variable to store the current position of the event.
{
"type": "VectorGraphic",
"source": "Pong",
"width": "${@matchFieldWidth}",
"height": "${@matchFieldHeight}",
"bind": [
{
"name": "playerPaddlePositionY",
"type": "number",
"value": "@initialPlayerPaddlePositionY"
}
],
"playerPaddlePositionY": "${playerPaddlePositionY}",
"onDown": [],
"onUp": [],
"onMove": []
}
We bind the variable "playerPaddlePositionY" with the initial position of our paddle to our VectorGraphic component and pass it to our graphic. It is necessary to register the variable to use it within the AVG definition. We add a parameters property, the value of the property is an array of objects or strings.
"graphics": {
"Pong": {
"parameters": ["playerPaddlePositionY"],
"type": "AVG",
"version": "1.1",
...
Furthermore we have to replace our resource (@initialPlayerPaddlePositionY) with the new variable (playerPaddlePaddlePositionY).
before | after |
---|---|
"description": "User Paddle", "translateX": "@initialPlayerPaddlePositionX", "translateY": "@initialPlayerPaddlePositionY", |
"description": "User Paddle", "translateX": "@initialPlayerPaddlePositionX", "translateY": "${playerPaddlePositionY}", |
We will have to change the graphic several times in the remaining steps. We have made all necessary preparations and can implement our handler logic.
"onMove": [
{
"type": "SetValue",
"property": "playerPaddlePositionY",
"value": "${Math.clamp(0, event.component.y - @paddleHeight, @matchFieldHeight)}"
}
]
We use the SetValue command to change the value of our variable "playerPaddlePositionY". Math.clamp is another feature that was introduced with the APL update. Math.clamp is an elegant notation for "Math.min(Math.max(value, min), max)" and ensures that the paddle only moves within the playing field.
A small step is still missing to make the player's paddle move. We have to implement "onUp" and "onDown". Since there is no use case for these handlers we will use the standard command "Idle".
"onDown": [
{
"type": "Idle"
}
],
"onUp": [
{
"type": "Idle"
}
],
That's it, we are able to move the paddle with the gesture control.
However, how can we move the ball or the enemy's paddle? The answer is: tick handlers.
Tick handlers allow you to periodically execute commands. Before we implement the tick handler, we define all the variables we need for the final step.
"type": "VectorGraphic",
"source": "Pong",
"width": "${@matchFieldWidth}",
"height": "${@matchFieldHeight}",
"bind": [
{
"name": "playerPaddlePositionY",
"type": "number",
"value": "@initialPlayerPaddlePositionY"
},
{
"name": "cpuPaddlePositionY",
"type": "number",
"value": "@initialCpuPaddlePositionY"
},
{
"name": "cpuScore",
"type": "number",
"value": "0"
},
{
"name": "playerScore",
"type": "number",
"value": "0"
},
{
"name": "pointOfCollision",
"type": "number",
"value": "0"
},
{
"name": "ballVelocityX",
"type": "number",
"value": "@initialBallVelocityX"
},
{
"name": "ballVelocityY",
"type": "number",
"value": "@initialBallVelocityY"
},
{
"name": "ballSpeed",
"type": "number",
"value": "@initialBallSpeed"
},
{
"name": "ballPositionX",
"type": "number",
"value": "@initialBallPositionX"
},
{
"name": "ballPositionY",
"type": "number",
"value": "@initialBallPositionY"
},
{
"name": "ballDirection",
"type": "string",
"value": "@initialBallDirection"
},
{
"name": "cpuPaddleSpeed",
"type": "number",
"value": "@initialCpuPaddleSpeed"
}
],
"playerPaddlePositionY": "${playerPaddlePositionY}",
We have defined variables for the score, the position/speed of the ball, the position of the enemy paddle and many more variables. We will pass all relevant variables to our graphic.
{
"type": "VectorGraphic",
"source": "Pong",
"width": "${@matchFieldWidth}",
"height": "${@matchFieldHeight}",
"bind": [
{
"name": "playerPaddlePositionY",
"type": "number",
"value": "@initialPlayerPaddlePositionY"
},
…
{
"name": "cpuPaddleSpeed",
"type": "number",
"value": "@initialCpuPaddleSpeed"
}
],
"playerPaddlePositionY": "${playerPaddlePositionY}",
"ballPositionX": "${ballPositionX}",
"ballPositionY": "${ballPositionY}",
"cpuScore": "${cpuScore}",
"playerScore": "${playerScore}",
"cpuPaddlePositionY": "${cpuPaddlePositionY}",
...
}
And also register the variables as parameters.
"Pong": {
"parameters": [
"playerPaddlePositionY",
"ballPositionX",
"ballPositionY",
"cpuScore",
"playerScore",
"cpuPaddlePositionY"
],
Furthermore we have to change some things in our graphic again, the table shows which of them:
before | after |
---|---|
"translateY": "@initialCpuPaddlePositionY", | "translateY": "${cpuPaddlePositionY}", |
"translateX": "@initialBallPositionX", "translateY": "@initialBallPositionY", |
"translateX": "${ballPositionX}", "translateY": "${ballPositionY}", |
"description": "Player Score", "text": "0", |
"description": "Player Score", "text": "${playerScore}", |
"description": "CPU Score", "text": "0", |
"description": "CPU Score", "text": "${cpuScore}", |
Our graphic should remain the same. We are now ready to implement the Tick Handler.
First we extend our VectorGraphic component with the handleTick property and define a tick handler.
{
"type": "VectorGraphic",
"source": "Pong",
…
"handleTick": [{
"minimumDelay": "${1000 / 60}",
"commands": []
}]
}
The minimumDelay property specifies the interval in which our commands should be executed. We want to update our field with a frame rate of 60 frames per second, we calculate this value with ${1000 / 60}, where 1000 stands for 1000 milliseconds (= 1 second) divided by the number of frames (60) per second.
Our tick handler will be a bit more complex, therefore it is recommended to consider using user-defined commands at this point.
User defined commands can be specified in the APL document in the commands section. We will call our command "UpdateCpuPaddlePosition".
{
"type": "APL",
"version": "1.4",
...
"commands": {
"UpdateCpuPaddlePosition": {
"commands": []
}
}
...
}
A custom command has 2 properties, a parameters property and a commands property.
We modify our tick handler to execute our custom command.
{
"type": "VectorGraphic",
"source": "Pong",
…
"handleTick": [{
"minimumDelay": "${1000 / 60}",
"commands": [{
"type": "UpdateCpuPaddlePosition"
}]
}
We are about to implement the logic that keeps the paddle of the enemy always aligned with the height of the ball:
"UpdateCpuPaddlePosition": {
"commands": [
{
"when": "${cpuPaddlePositionY > (ballPositionY - (@paddleHeight / 2))}",
"type": "Select",
"commands": [
{
"when": "${ballDirection == @ballDirectionRight}",
"type": "SetValue",
"property": "cpuPaddlePositionY",
"value": "${cpuPaddlePositionY - (cpuPaddleSpeed / 1.5)}"
}
],
"otherwise": [
{
"type": "SetValue",
"property": "cpuPaddlePositionY",
"value": "${cpuPaddlePositionY - (cpuPaddleSpeed / 4)}"
}
]
},
{
"when": "${cpuPaddlePositionY < (ballPositionY - (@paddleHeight / 2))}",
"type": "Select",
"commands": [
{
"when": "${ballDirection == @ballDirectionRight}",
"type": "SetValue",
"property": "cpuPaddlePositionY",
"value": "${cpuPaddlePositionY + (cpuPaddleSpeed / 1.5)}"
}
],
"otherwise": [
{
"type": "SetValue",
"property": "cpuPaddlePositionY",
"value": "${cpuPaddlePositionY + (cpuPaddleSpeed / 4)}"
}
]
},
{
"when": "${(cpuPaddlePositionY + @paddleHeight) > (@matchFieldHeight - @wallHeight)}",
"type": "SetValue",
"property": "cpuPaddlePositionY",
"value": "${@matchFieldHeight - @wallHeight - @paddleHeight}"
},
{
"when": "${cpuPaddlePositionY < @wallHeight}",
"type": "SetValue",
"property": "cpuPaddlePositionY",
"value": "@wallHeight"
}
]
}
Our UpdateCpuPaddlePosition command has 4 blocks that are executed under certain conditions. The first two blocks cause the paddle of the enemy to move up and down. With the Select command, which was introduced with APL 1.3, we achieve an if/else statement. If none of the conditions defined in the commands array have been fulfilled, the commands defined in the otherwise array will be executed.
Through the last two blocks we make sure that the paddle of the enemy always stays inside the field.
Now it’s time to get the ball rolling (literally!).
The "physics" of the game
For the final step we will need a number of additional user-defined commands. We will call these UpdateBallPosition, CheckCollision, CheckEndOfRound and ResetGame.
"commands": {
"UpdateCpuPaddlePosition": {
"commands": [...]
},
"UpdateBallPosition": {
"commands": []
},
"CheckCollision": {
"commands": []
},
"CheckEndOfRound": {
"commands": []
},
"ResetGame": {
"commands": []
}
}
Aside from "ResetGame", all commands should be executed in our tick handler:
"handleTick": [
{
"minimumDelay": "${1000 / 60}",
"commands": [
{
"type": "CheckCollision"
},
{
"type": "CheckEndOfRound"
},
{
"type": "UpdateBallPosition"
},
{
"type": "UpdateCpuPaddlePosition"
}
]
}
]
We first implement our UpdateBallPosition command:
"UpdateBallPosition": {
"commands": [
{
"type": "SetValue",
"property": "ballPositionX",
"value": "${ballPositionX + ballVelocityX}"
},
{
"type": "SetValue",
"property": "ballPositionY",
"value": "${ballPositionY + ballVelocityY}"
},
{
"when": "${ballPositionY - @ballRadius < @wallHeight || ballPositionY + @ballRadius > (@matchFieldHeight - @wallHeight)}",
"description": "ball hits the wall",
"type": "SetValue",
"property": "ballVelocityY",
"value": "${ballVelocityY * -1}"
}
]
},
With each "tick" we update the x and y coordinates of our ball, we also check if the ball has hit the top or bottom wall, in this case we negate our Y velocity to move the ball in a different direction.
Next we implement ResetGame:
"ResetGame": {
"commands": [
{
"type": "SetValue",
"property": "ballPositionX",
"value": "@initialBallPositionX"
},
{
"type": "SetValue",
"property": "ballPositionY",
"value": "@initialBallPositionY"
},
{
"type": "SetValue",
"property": "ballSpeed",
"value": "@initialBallSpeed"
},
{
"type": "SetValue",
"property": "ballVelocityY",
"value": "@initialBallVelocityY"
}
]
}
This command resets the speed, velocity and position of the ball to the initial values.
Our ball moves across the screen and also bounces off the wall, but then disappears from the screen. Therefore we will implement the CheckEndOfRound command next.
"CheckEndOfRound": {
"commands": [
{
"when": "${ballPositionX - @ballRadius < 0}",
"type": "Sequential",
"commands": [
{
"type": "ResetGame"
},
{
"type": "SetValue",
"property": "cpuScore",
"value": "${cpuScore + 1}"
}
]
},
{
"when": "${(ballPositionX - @ballRadius) > @matchFieldWidth}",
"type": "Sequential",
"commands": [
{
"type": "ResetGame"
},
{
"type": "SetValue",
"property": "playerScore",
"value": "${playerScore + 1}"
},
{
"type": "SetValue",
"property": "cpuPaddleSpeed",
"value": "${cpuPaddleSpeed + 1}"
}
]
}
]
},
Depending on which side the ball leaves the screen, either the player or the computer wins. In both cases our ResetGame command is executed and the winner of the round gets one point. Moreover, our enemy gets better with each game round the player wins.
It's time for the last command to complete our Pong game! The CheckCollision command ensures that our paddles are not completely useless.
"CheckCollision": {
"parameters": [
{
"name": "paddlePositionX",
"default": "${ballDirection == @ballDirectionLeft ? (@playerPaddlePositionX + @paddleWidth) : (@cpuPaddlePositionX - @paddleWidth)}"
},
{
"name": "paddlePositionY",
"default": "${ballDirection == @ballDirectionLeft ? playerPaddlePositionY : cpuPaddlePositionY}"
},
{
"name": "isWithinX",
"default": "${(ballPositionX - @ballRadius) <= paddlePositionX && ballPositionX >= (paddlePositionX - @paddleWidth)}"
},
{
"name": "isWithinY",
"default": "${ballPositionY <= (paddlePositionY + @paddleHeight) && (ballPositionY + @ballRadius) >= paddlePositionY}"
}
],
"commands": [
{
"when": "${isWithinX && isWithinY}",
"type": "Sequential",
"commands": [
{
"type": "SetValue",
"property": "pointOfCollision",
"value": "${(ballPositionY - (paddlePositionY + @paddleHeight / 2)) / (@paddleHeight / 2)}"
},
{
"type": "SetValue",
"property": "ballVelocityX",
"value": "${((ballPositionX + @ballRadius < @matchFieldHeight / 2) ? 1 : -1) * ballSpeed * Math.cos((Math.PI / 4) * pointOfCollision)}"
},
{
"type": "SetValue",
"property": "ballVelocityY",
"value": "${ballSpeed * Math.sin((Math.PI / 4) * pointOfCollision)}"
},
{
"type": "SetValue",
"property": "ballSpeed",
"value": "${Math.clamp(0, ballSpeed + 1, 25)}"
},
{
"type": "SetValue",
"property": "ballDirection",
"value": "${ballDirection == @ballDirectionRight ? @ballDirectionLeft : @ballDirectionRight}"
}
]
}
]
},
This command is by far the most complex compared to the others. As I mentioned before, a parameter can be defined either as a string or an object. So far we have only used strings to register our parameters, but in this case we will use an object.
The advantage is that we can define a default value. Of course, it would also be possible to specify the parameters only as string, but for this we would have to extend our tick handler and double our CheckCollision command and also pass the correct values.
Our current CheckCollision call:
{
"type": "CheckCollision"
},
would look like this:
{
"when": "${ballDirection == @ballDirectionRight}",
"type": "CheckCollision",
"paddlePositionX: "${@cpuPaddlePositionX - @paddleWidth}",
"paddlePositionY": "${cpuPaddlePositionY}",
"isWithinX": "${(ballPositionX - @ballRadius) <= @cpuPaddlePositionX - @paddleWidth && ballPositionX >= (@cpuPaddlePositionX - @paddleWidth - @paddleWidth)}",
"isWithinY": "${ballPositionY <= (cpuPaddlePositionY + @paddleHeight) && (ballPositionY + @ballRadius) >= cpuPaddlePositionY}"
},
{
"when": "${ballDirection == @ballDirectionLeft}",
"type": "CheckCollision",
"paddlePositionX: "${@playerPaddlePositionX - @paddleWidth}",
"paddlePositionY": "${playerPaddlePositionY}",
"isWithinX": "${(ballPositionX - @ballRadius) <= @playerPaddlePositionX - @paddleWidth && ballPositionX >= (@playerPaddlePositionX - @paddleWidth - @paddleWidth)}",
"isWithinY": "${ballPositionY <= (playerPaddlePositionY + @paddleHeight) && (ballPositionY + @ballRadius) >= playerPaddlePositionY}"
}
Quite confusing, isn't it? Plus, we have so much redundant code!
Let's get back to our CheckCollision implementation. We have defined 4 parameters (paddlePositionX, paddlePositionY, isWithinX and isWithinY) which get their value from the default property.
We also execute a number of commands when the ball hits the paddle. This is ensured by the variables isWithinX and isWithinY.
We store the point where the ball hits the paddle to determine the exit angle. We also increase the speed of the ball and we use the Math.clamp function to make sure that our ball doesn't get too fast and last but not least we update the direction in which the ball flies.
We're done!
You can find the complete APL document and many more in my GitHub repository.
I hope you enjoyed the blog article. I appreciate your feedback on Twitter (@xeladotbe ) as well as interesting APL discussions in general.
Top comments (0)