CrossBrowdy - Examples

Advanced

Pong game

This is an example of a simple Pong game (using the Game engine module from the previous example).

This game is for two human players. You can use different parts of the keyboard or different parts of the screen (with the mouse or with a touch screen) to control each player. It is also possible to use up to two gamepads to control the players.

The purpose of this example is to show different ways to use CrossBrowdy and some of its multiple features.

index.html:

<!DOCTYPE html>
<html>
	<head>
		<!-- This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. -->
		<meta http-equiv="content-type" content="text/html; charset=utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
		<link rel="canonical" href="https://crossbrowdy.com/examples/advanced/pong_game/try" />
		<title>Advanced: Pong game - Example</title>
		<!-- Loads the needed CSS files: -->
		<link rel="stylesheet" type="text/css" href="main.css" />
		<!-- Loads FlashCanvas (Flash emulation) before CrossBrowdy. Needed also to use ExplorerCanvas (VML emulation) without problems: -->
		<!-- Note: it is recommended to download CrossBrowdy instead of hotlinking the online version. This is just for the example! -->
		<script src="https://crossbrowdy.com/CrossBrowdy/CrossBase/audiovisual/image/canvas/FlashCanvas/pro/bin/flashcanvas.js" type="text/javascript" language="javascript"></script><!-- FlashCanvas/ExplorerCanvas do not support lazy load. -->
		<!-- Loads CrossBrowdy.js (main file): -->
		<!-- Note: it is recommended to download CrossBrowdy instead of hotlinking the online version. This is just for the example! -->
		<script src="https://crossbrowdy.com/CrossBrowdy/CrossBrowdy.js" type="text/javascript" language="javascript"></script><!-- "type" and "language" parameters for legacy clients. -->
		<!-- Loads the other needed script files: -->
		<script src="main.js" type="text/javascript" language="javascript"></script>
	</head>
	<body>
		<div id="loading">Loading...</div>
		<div id="debug_switch"><label for="debug_checkbox"><input type="checkbox" id="debug_checkbox" name="debug_checkbox" />Debug</label></div>
		<div id="goal">Goal!</div>
		<canvas id="my_canvas">if you read this, canvas is not working</canvas><!-- Some emulation methods will require the canvas element created in HTML (not dynamically by JavaScript). -->
		<canvas id="my_canvas_buffer">if you read this, canvas is not working</canvas><!-- Some emulation methods will require the canvas element created in HTML (not dynamically by JavaScript). -->
		<button id="start_button" onClick="gameStart();">
			<span style="display:block; margin-bottom:10px; color:#00cc88;">Frog's Theme (Chrono Trigger) by Yasunori Mitsuda (version by Jarred Mack and Cody Lundquist using Band.js library)</span>
			<span style="display:block; margin-bottom:10px; color:#cc0088;">This example belongs to CrossBrowdy.com, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License.</span>
			<span style="display:block; margin-bottom:10px;">
				This game is for two human players. You can use different parts of the keyboard or different parts of the screen (with the mouse or with a touch screen) to control each player. It is also possible to use up to two gamepads to control the players.
			</span>
			<span style="display:block; margin-top:10px;">Start game!</span>
		</button>
		<br />
		<!-- The "CB_console" element will be used automatically in the case that the client does not support console: -->
		<div id="CB_console" style="display:none; visibility:hidden; overflow:scroll;">
			<span style="font-weight:bold;">Console:</span><br />
		</div>
		<div id="crossbrowdy_info"><a href="https://crossbrowdy.com/examples/advanced/pong_game" target="_blank">CrossBrowdy.com example</a></div>
	</body>
</html>

main.css:

/* This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. */

body { background-color:#333333; word-wrap:break-word; }
#crossbrowdy_info { position:fixed; bottom:2px; right:2px; color:#bbbbbb; }
#crossbrowdy_info a { color:#00aadd; }
#crossbrowdy_info a:hover { color:#0033aa; }
#CB_console { width:460px; height:100px; background-color:#aaaaaa; color:#ddddff; }
span { color:#aa0000; }
#debug_switch { position:absolute; top:0px; right:6px; z-index:2; color:#ffaa00; }
#debug_switch, label, #debug_checkbox  { cursor:hand; cursor:pointer; }
#loading { position:absolute; left:0px; top:0px; width:100%; height:100%; color:#ffff00; background-color:#000000; }
#my_canvas { position:absolute; left:0px; top:0px; cursor:crosshair; }
#my_canvas_buffer { position:absolute; left:0px; top:0px; visibility:hidden; display:none; cursor:crosshair; }
#start_button
{
	visibility: hidden;
	display: none;
	position:absolute;
	left:10%;
	top:10%;
	width:80%;
	height:80%;
	color:#ff0000;
	font-size:12px !important;
	font-size:0.58em !important;
	font-size:58% !important;
	font-size:2.8vmin !important;
	font-weight:bold;
	cursor:pointer;
	cursor:hand;
	filter:alpha(opacity=80);
	opacity:0.8;
	-moz-opacity:0.8;
	-khtml-opacity:0.8;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
}
#start_button:hover
{
	color:#ffaa00;
	filter:alpha(opacity=100);
	opacity:1;
	-moz-opacity:1;
	-khtml-opacity:1;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
}
#goal
{
	z-index:4;
	visibility:hidden;
	display:none;
	position:absolute;
	left:50%;
	top:50%;
	width:50%;
	height:50%;
	color:#ff0000;
	font-weight:bold;
	filter:alpha(opacity=90);
	opacity:0.9;
	-moz-opacity:0.9;
	-khtml-opacity:0.9;
	-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=90)";
	animation:blinking 0.3s ease-in-out infinite;
}
@keyframes blinking
{
	0%
	{
		filter:alpha(opacity=70);
		opacity:0.7;
		-moz-opacity:0.7;
		-khtml-opacity:0.7;
		-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
	}

	50%
	{
		filter:alpha(opacity=0);
		opacity:0;
		-moz-opacity:0;
		-khtml-opacity:0;
		-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
	}

	100%
	{
		filter:alpha(opacity=70);
		opacity:0.7;
		-moz-opacity:0.7;
		-khtml-opacity:0.7;
		-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
	}
}

main.js:

/* This file belongs to a CrossBrowdy.com example, made by Joan Alba Maldonado. Creative Commons Attribution 4.0 International License. */

//Path to the graphic rendering engine module:
var CB_GEM_PATH = CB_GEM_PATH || "../simple_game_engine_files/";

//Defines whether to shows debug messages or not:
var CB_GEM_DEBUG_MESSAGES = true;

//Adds the game engine module to CrossBrowdy:
var CB_GEM_MODULE_NEEDED_MODULES = {};
CB_GEM_MODULE_NEEDED_MODULES[CB_GEM_PATH + "game_engine_module.js"] = { load: true, mandatory: true, absolutePath: true };
CB_Modules.addNeededModule(CB_NAME, "GAME_ENGINE_MODULE", CB_GEM_MODULE_NEEDED_MODULES);


CB_init(main); //It will call the "main" function when ready.


//This function will be called when CrossBrowdy is ready:
function main()
{
	CB_console("CrossBrowdy and all needed modules loaded. Starting game...");
	
	//Prepares the background music (to play it later):
	prepareMusic();
	
	//Defines needed data:
	var goalsToNextLevel = 3;
	var ballSpeedMsInitial = 16;
	var ballSpeedMsDecrement = 1; //The number of milliseconds needed to move the ball each step which will be decremented in each level (so it will be faster).
	var ballMovePercentageHorizontalInitial = 0.006; //Number of percentage of the field that the ball will move horizontally at the beginning, in each step.
	var ballMovePercentageVerticalInitial = 0.003; //Number of percentage of the field to move the ball vertically at the beginning, in each step.
	CB_GEM.data = //Data stored in the game engine module (can be exported to save the game status):
	{
		//Note: most of the data will be calculated automatically and dynamically.
		soundEnabled: true, //Set to false to disable sound.
		gameStarted: false,
		level: 1,
		field:
		{
			top: 0,
			left: 0,
			width: 0,
			height: 0
		},
		player1:
		{
			goals: 0,
			x: 0,
			y: 0,
			width: 0,
			height: 0
		},
		player2:
		{
			goals: 0,
			x: 0,
			y: 0,
			width: 0,
			height: 0
		},
		ball:
		{
			speedMsInitial: ballSpeedMsInitial,
			speedMs: ballSpeedMsInitial,
			movePercentageHorizontalInitial: ballMovePercentageHorizontalInitial,
			movePercentageHorizontal: ballMovePercentageHorizontalInitial,
			movePercentageVerticalInitial: ballMovePercentageVerticalInitial,
			movePercentageVertical: ballMovePercentageVerticalInitial,
			x: 0,
			y: 0,
			radius: 0,
			direction: 0, //0 = not moving (stuck to a paddle), 1 = left to right, 2 = right to left.
			firstPlayer: Math.floor(Math.random() * 2) + 1 //1 or 2 (random).
		}
	};
	
	//Sets the desired sprites scene data (can be modified dynamically):
	var fieldLineWidth = 10;
	CB_GEM.spritesGroupsData =
	{
		//'my_sprites_groups_1' ('CB_GraphicSpritesScene.SPRITES_GROUPS_OBJECT' object). Some missing or non-valid properties will get a default value:
		id: "my_sprites_groups_1", //Identifier of the sprites groups object (also used for the 'CB_GraphicSpritesScene' object). Optional but recommended. It should be unique. By default, it is generated automatically.
		//Numeric array containing 'CB_GraphicSprites.SPRITES_OBJECT' objects with all the sprites groups that will be used (their "parent" property will be set to point the current 'CB_GraphicSpritesScene' object which contains them):
		spritesGroups:
		[
			{
				id: "info",
				srcType: CB_GraphicSprites.SRC_TYPES.TEXT,
				top: 15,
				zIndex: 3,
				data:
				{
					fontSize: "16px",
					fontFamily: "courier",
					style: "#ffff00",
					fontWeight: "bold"
				},
				sprites: [ { id: "info_sprite" } ]
			},
			{
				id: "field_group",
				srcType: CB_GraphicSprites.SRC_TYPES.RECTANGLE,
				sprites:
				[
					{
						id: "field",
						left: null, //It will be calculated dynamically.
						top: null, //It will be calculated dynamically.
						width: null, //It will be calculated dynamically.
						height: null, //It will be calculated dynamically.
						data:
						{
							stroke: true,
							lineWidth: fieldLineWidth,
							style: function(element, canvasContext, canvasBufferContext, userBuffer) { return getGradientStyle("#aa0000", "#aaaa00", element, canvasContext); }
						},
						subSprites:
						[
							{
								id: "field_middle",
								srcType: CB_GraphicSprites.SRC_TYPES.SEGMENT,
								data:
								{
									x1: null, //It will be calculated dynamically.
									y1: null, //It will be calculated dynamically.
									x2: null, //It will be calculated dynamically.
									y2: null, //It will be calculated dynamically.
									style: function(element, canvasContext, canvasBufferContext, userBuffer) { return getGradientStyle("#aa4400", "#aa6600", element, canvasContext); }
								}
							}
						]
					}
				]
			},
			{
				id: "paddles_group",
				srcType: CB_GraphicSprites.SRC_TYPES.RECTANGLE,
				sprites:
				[
					{
						id: "paddles",
						data:
						{
							hidden: true,
							lineWidth: fieldLineWidth,
							style: function(element, canvasContext, canvasBufferContext, userBuffer) { return getGradientStyle("#00aa00", "#aaaa00", element, canvasContext); }
						},
						subSprites:
						[
							{
								id: "paddle_1",
								left: null, //It will be calculated dynamically.
								top: null, //It will be calculated dynamically.
								width: null, //It will be calculated dynamically.
								height: null, //It will be calculated dynamically
								data: { hidden: false }
							},
							{
								id: "paddle_2",
								left: null, //It will be calculated dynamically.
								top: null, //It will be calculated dynamically.
								width: null, //It will be calculated dynamically.
								height: null, //It will be calculated dynamically
								data: { hidden: false }
							}
						]
					}
				]
			},
			{
				id: "ball_group",
				srcType: CB_GraphicSprites.SRC_TYPES.CIRCLE,
				sprites:
				[
					{
						id: "ball",
						data:
						{
							radius: null, //It will be calculated dynamically.
							opacity: 0.5,
							style: function(element, canvasContext, canvasBufferContext, userBuffer) { return getGradientStyle("#00aa00", "#aaaa00", element, canvasContext); }
						}
					}
				]
			}
		]
	};
	
	//Defines the events to detect the pointer input and capture the coordinates:
	setPointerEvents();

	//Defines the callbacks for the game loop:
	var timingLastTime = 0;
	var timingNow = null;
	CB_GEM.onLoopStart = function(graphicSpritesSceneObject, CB_REM_dataObject, expectedCallingTime) //When the game loop starts, before rendering the graphics (if it returns false, it will skip rendering in this loop):
	{
		//Manages input:
		manageInput();
		
		//If the game has not started, exits:
		if (!CB_GEM.data.gameStarted) { return; }
		//...otherwise, if the ball is stuck to a paddle, exits:
		else if (CB_GEM.data.ball.direction === 0) { return; } //0 = not moving (stuck to a paddle), 1 = left to right, 2 = right to left.
		//...otherwise (the ball is released, not stuck to a paddle), the ball should be moving so proceeds:
		else
		{
			//Gets the current timing:
			timingNow = CB_Device.getTiming();
			
			//If enough time has elapsed, moves the ball:
			if (timingNow - timingLastTime >= CB_GEM.data.ball.speedMs)
			{
				//Moves the ball vertically:
				graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y += CB_GEM.data.ball.movePercentageVertical * CB_GEM.data.field.height;

				//Calculates goals and collisions with the paddles:
				if (CB_GEM.data.ball.direction === 1) //Ball goes left to right.
				{
					//Moves the ball horizontally:
					graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x += CB_GEM.data.ball.movePercentageHorizontal * CB_GEM.data.field.width;
					
					//Calculates if a goal was scored:
					if (CB_GEM.data.ball.x + CB_GEM.data.ball.radius >= CB_GEM.data.field.left + CB_GEM.data.field.width)
					{
						CB_GEM.data.player1.goals++; //Winner scores a goal.
						CB_GEM.data.ball.direction = 0; //Stops the ball.
						CB_GEM.data.ball.movePercentageVertical = CB_GEM.data.ball.movePercentageVerticalInitial;
						if (Math.floor(Math.random() * 2) === 0) { CB_GEM.data.ball.movePercentageVertical *= -1; } //It starts moving down or up, randomly.
						CB_GEM.data.ball.firstPlayer = 2; //Next turn will be started by the loser.
						//Sticks the ball to the loser's paddle:
						graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x = CB_GEM.data.player2.x - CB_GEM.data.ball.radius; //Sets the ball horizontally to the limit allowed.
						graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.player2.y + CB_GEM.data.player2.height / 2; //Sets the ball vertically in the middle of the paddle.
						
						CB_Device.Vibration.start(100); //Makes the device vibrate.
						playSoundFx("goal"); //Plays the goal sound.
						
						//Shows the "Goal!" message:
						showGoalMessage();
					}
					//...otherwise, calculates collision with the front part of the player 2's paddle:
					else if (CB_Collisions.isLineTouchingCircle(CB_GEM.data.player2.x, CB_GEM.data.player2.y, CB_GEM.data.player2.x, CB_GEM.data.player2.y + CB_GEM.data.player2.height, CB_GEM.data.ball.x, CB_GEM.data.ball.y, CB_GEM.data.ball.radius))
					{
						graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x = CB_GEM.data.player2.x - CB_GEM.data.ball.radius; //Sets the ball horizontally to the limit allowed.
						
						//The ball direction changes:
						CB_GEM.data.ball.direction = 2; //Ball will go from right to left.
						
						//Inclination of the ball depends on the part of the paddle hit:
						var verticalDistance = CB_Collisions.getDistancePoints(0, CB_GEM.data.ball.y, 0, CB_GEM.data.player2.y + CB_GEM.data.player2.height / 2);
						var verticalIncination = 5;
						if (verticalDistance <= CB_GEM.data.player2.height / 16) { verticalIncination = 1; }
						else if (verticalDistance <= CB_GEM.data.player2.height / 8) { verticalIncination = 2; }
						else if (verticalDistance <= CB_GEM.data.player2.height / 6) { verticalIncination = 3; }
						else if (verticalDistance <= CB_GEM.data.player2.height / 4) { verticalIncination = 4; }
						if (CB_GEM.data.ball.movePercentageVertical < 0) { verticalIncination *= -1; }
						CB_GEM.data.ball.movePercentageVertical = verticalIncination * Math.abs(CB_GEM.data.ball.movePercentageVerticalInitial);
						
						playSoundFx("hit_paddle"); //Plays the paddle hit sound.
					}
					//...otherwise, calculates collision with the upper part (horizontal side) of the player 2's paddle:
					else if (CB_Collisions.isLineTouchingCircle(CB_GEM.data.player2.x, CB_GEM.data.player2.y, CB_GEM.data.player2.x + CB_GEM.data.player2.width, CB_GEM.data.player2.y, CB_GEM.data.ball.x, CB_GEM.data.ball.y, CB_GEM.data.ball.radius))
					{
						graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.player2.y - CB_GEM.data.ball.radius; //Sets the ball vertically to the limit allowed.
						
						//If the ball was going down, changes its direction to go up:
						if (CB_GEM.data.ball.movePercentageVertical < 0) { CB_GEM.data.ball.movePercentageVertical *= -1; }
						//...otherwise, if the ball was going up:
						else {  } //TO DO: not done to keep the example simpler.
						
						playSoundFx("hit_paddle"); //Plays the paddle hit sound.
					}
					//...otherwise, calculates collision with the bottom part (horizontal side) of the player 2's paddle:
					else if (CB_Collisions.isLineTouchingCircle(CB_GEM.data.player2.x, CB_GEM.data.player2.y + CB_GEM.data.player2.height, CB_GEM.data.player2.x + CB_GEM.data.player2.width, CB_GEM.data.player2.y + CB_GEM.data.player2.height, CB_GEM.data.ball.x, CB_GEM.data.ball.y, CB_GEM.data.ball.radius))
					{
						graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.player2.y + CB_GEM.data.player2.height + CB_GEM.data.ball.radius; //Sets the ball vertically to the limit allowed.
						
						//If the ball was going up, changes its direction to go down:
						if (CB_GEM.data.ball.movePercentageVertical > 0) { CB_GEM.data.ball.movePercentageVertical *= -1; }
						//...otherwise, if the ball was going down:
						else {  } //TO DO: not done to keep the example simpler.
						
						playSoundFx("hit_paddle"); //Plays the paddle hit sound.
					}
				}
				else if (CB_GEM.data.ball.direction === 2) //Ball goes right to left.
				{
					//Moves the ball horizontally:
					graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x -= CB_GEM.data.ball.movePercentageHorizontal * CB_GEM.data.field.width;
					
					//Calculates if a goal was scored:
					if (CB_GEM.data.ball.x + CB_GEM.data.ball.radius <= CB_GEM.data.field.left)
					{
						CB_GEM.data.player2.goals++; //Winner scores a goal.
						CB_GEM.data.ball.direction = 0; //Stops the ball.
						CB_GEM.data.ball.movePercentageVertical = CB_GEM.data.ball.movePercentageVerticalInitial;
						if (Math.floor(Math.random() * 2) === 0) { CB_GEM.data.ball.movePercentageVertical *= -1; } //It starts moving down or up, randomly.
						CB_GEM.data.ball.firstPlayer = 1; //Next turn will be started by the loser.
						//Sticks the ball to the loser's paddle:
						graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x = CB_GEM.data.player1.x + CB_GEM.data.player1.width + CB_GEM.data.ball.radius; //Sets the ball horizontally to the limit allowed.
						graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.player1.y + CB_GEM.data.player1.height / 2; //Sets the ball vertically in the middle of the paddle.

						CB_Device.Vibration.start(100); //Makes the device vibrate.
						playSoundFx("goal"); //Plays the goal sound.
						
						//Shows the "Goal!" message:
						showGoalMessage();
					}
					//...otherwise, calculates collision with the front part of the  player 1's paddle:
					else if (CB_Collisions.isLineTouchingCircle(CB_GEM.data.player1.x + CB_GEM.data.player1.width, CB_GEM.data.player1.y, CB_GEM.data.player1.x + CB_GEM.data.player1.width, CB_GEM.data.player1.y + CB_GEM.data.player1.height, CB_GEM.data.ball.x, CB_GEM.data.ball.y, CB_GEM.data.ball.radius))
					{
						graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x = CB_GEM.data.player1.x + CB_GEM.data.player1.width + CB_GEM.data.ball.radius; //Sets the ball horizontally to the limit allowed.
						
						//The ball direction changes:
						CB_GEM.data.ball.direction = 1; //Ball will go from left to right.

						//Inclination of the ball depends on the part of the paddle hit:
						var verticalDistance = CB_Collisions.getDistancePoints(0, CB_GEM.data.ball.y, 0, CB_GEM.data.player1.y + CB_GEM.data.player1.height / 2);
						var verticalIncination = 5;
						if (verticalDistance <= CB_GEM.data.player2.height / 16) { verticalIncination = 1; }
						else if (verticalDistance <= CB_GEM.data.player2.height / 8) { verticalIncination = 2; }
						else if (verticalDistance <= CB_GEM.data.player2.height / 6) { verticalIncination = 3; }
						else if (verticalDistance <= CB_GEM.data.player2.height / 4) { verticalIncination = 4; }
						if (CB_GEM.data.ball.movePercentageVertical < 0) { verticalIncination *= -1; }
						CB_GEM.data.ball.movePercentageVertical = verticalIncination * Math.abs(CB_GEM.data.ball.movePercentageVerticalInitial);
						
						playSoundFx("hit_paddle"); //Plays the paddle hit sound.
					}
					//...otherwise, calculates collision with the upper part (horizontal side) of the player 1's paddle:
					else if (CB_Collisions.isLineTouchingCircle(CB_GEM.data.player1.x, CB_GEM.data.player1.y, CB_GEM.data.player1.x + CB_GEM.data.player1.width, CB_GEM.data.player1.y, CB_GEM.data.ball.x, CB_GEM.data.ball.y, CB_GEM.data.ball.radius))
					{
						graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.player1.y - CB_GEM.data.ball.radius; //Sets the ball vertically to the limit allowed.
						
						//If the ball was going down, changes its direction to go up:
						if (CB_GEM.data.ball.movePercentageVertical < 0) { CB_GEM.data.ball.movePercentageVertical *= -1; }
						//...otherwise, if the ball was going up:
						else {  } //TO DO: not done to keep the example simpler.
						
						playSoundFx("hit_paddle"); //Plays the paddle hit sound.
					}
					//...otherwise, calculates collision with the bottom part (horizontal side) of the player 1's paddle:
					else if (CB_Collisions.isLineTouchingCircle(CB_GEM.data.player1.x, CB_GEM.data.player1.y + CB_GEM.data.player1.height, CB_GEM.data.player1.x + CB_GEM.data.player1.width, CB_GEM.data.player1.y + CB_GEM.data.player1.height, CB_GEM.data.ball.x, CB_GEM.data.ball.y, CB_GEM.data.ball.radius))
					{
						graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.player1.y + CB_GEM.data.player1.height + CB_GEM.data.ball.radius; //Sets the ball vertically to the limit allowed.
						
						//If the ball was going up, changes its direction to go down:
						if (CB_GEM.data.ball.movePercentageVertical > 0) { CB_GEM.data.ball.movePercentageVertical *= -1; }
						//...otherwise, if the ball was going down:
						else {  } //TO DO: not done to keep the example simpler.
						
						playSoundFx("hit_paddle"); //Plays the paddle hit sound.
					}
				}
				
				//Calculates ball collision with the top or bottom parts of the field:
				if (CB_GEM.data.ball.movePercentageVertical < 0 && CB_GEM.data.ball.y - CB_GEM.data.ball.radius <= CB_GEM.data.field.top + CB_GEM.data.field.fieldLineWidth / 2) //The ball has hit the top part.
				{
					graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.field.top + CB_GEM.data.field.fieldLineWidth / 2 + CB_GEM.data.ball.radius; //Sets the ball vertically to the limit allowed.
					CB_GEM.data.ball.movePercentageVertical *= -1; //Reverts it (from up to down).
					playSoundFx("hit_border"); //Plays the border hit sound.
				}
				if (CB_GEM.data.ball.movePercentageVertical > 0 && CB_GEM.data.ball.y + CB_GEM.data.ball.radius >= CB_GEM.data.field.top + CB_GEM.data.field.height - CB_GEM.data.field.fieldLineWidth / 2) //The ball has hit the bottom part.
				{
					graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data.field.top + CB_GEM.data.field.height - CB_GEM.data.field.fieldLineWidth / 2 - CB_GEM.data.ball.radius; //Sets the ball vertically to the limit allowed.
					CB_GEM.data.ball.movePercentageVertical *= -1; //Reverts it (from down to up).
					playSoundFx("hit_border"); //Plays the border hit sound.
				}
				
				timingLastTime = timingNow; //Updates the last timing when the ball was moved for the last time.
				updateInfo(graphicSpritesSceneObject); //Updates the information shown.
			}
		}
	};
	
	CB_GEM.onLoopEnd = function(graphicSpritesSceneObject, CB_REM_dataObject, expectedCallingTime) //When the game loop ends, after rendering the graphics (not executed if the 'CB_GEM.onLoopStart' function returned false):
	{
		if (!CB_GEM.data.gameStarted) { return; }
		
		//If any of the players has reached the needed goals to jump to the next level, proceeds:
		if (CB_GEM.data.player1.goals >= goalsToNextLevel || CB_GEM.data.player2.goals >= goalsToNextLevel)
		{
			CB_GEM.data.player1.goals = CB_GEM.data.player2.goals = 0; //Resets goals for everyone.
			CB_GEM.data.level++; //Increases the level.
			CB_GEM.data.ball.speedMs -= ballSpeedMsDecrement; //Decrements the milliseconds needed to move the ball each step (increments speed).
			if (CB_GEM.data.ball.speedMs < 0) { CB_GEM.data.ball.speedMs = 0; }
			updateInfo(graphicSpritesSceneObject); //Updates the information shown.
		}
	};
	
	//Starts the game engine module:
	CB_GEM.begin
	(
		//onStart:
		function(graphicSpritesSceneObject, CB_CanvasObject, CB_CanvasObjectBuffer, FPSSprites) //'FPSSprites' contains the 'CB_GraphicSprites.SPRITES_OBJECT' object used to display the FPS counter.
		{
			FPSSprites.setDisabled(false); //Set to true to hide FPS counter.
	
			resizeElements(graphicSpritesSceneObject, true); //Updates all visual elements according to the screen size.

			updateInfo(graphicSpritesSceneObject); //Shows the information for the first time.
	
			//Updates all when the screen is resized or changes its orientation:
			CB_GEM.onResize = function(graphicSpritesSceneObject, CB_REM_dataObject, CB_CanvasObject, CB_CanvasObjectBuffer)
			{
				resizeElements(graphicSpritesSceneObject);
				updateInfo(graphicSpritesSceneObject);
			};
			
			CB_Elements.hideById("loading"); //Hides the loading message.
			CB_Elements.showById("start_button"); //Shows the start button.
			
			//Sets the event for the debug checkbox:
			var debugCheckbox = CB_Elements.id("debug_checkbox");
			if (debugCheckbox !== null)
			{
				debugCheckbox.checked = !!updateInfoDebug;
				CB_Events.on(debugCheckbox, "change", function() { updateInfoDebug = !!debugCheckbox.checked; });
			}
		},
		
		//onError:
		function(error) { CB_console("Error: " + error); }
	);
}


//Starts the game:
function gameStart(graphicSpritesSceneObject)
{
	if (CB_GEM.data.gameStarted) { return; }

	graphicSpritesSceneObject = graphicSpritesSceneObject || CB_GEM.graphicSpritesSceneObject;
	if (!graphicSpritesSceneObject) { return; }

	hideGoalMessage(); //Hides the "Goal!" message (just in case).
	
	//Hides the start button:
	CB_Elements.hideById("start_button");
	
	//Resets data:
	CB_GEM.data.player1.goals = CB_GEM.data.player2.goals = 0;
	CB_GEM.data.level = 1;
	CB_GEM.data.ball.speedMs = CB_GEM.data.ball.speedMsInitial; 
	CB_GEM.data.ball.movePercentageHorizontal = CB_GEM.data.ball.movePercentageHorizontalInitial;
	CB_GEM.data.ball.movePercentageVertical = CB_GEM.data.ball.movePercentageVerticalInitial;
	if (Math.floor(Math.random() * 2) === 0) { CB_GEM.data.ball.movePercentageVertical *= -1; } //It starts moving down or up, randomly.
	CB_GEM.data.ball.direction = 0; //0 = not moving (stuck to a paddle), 1 = left to right, 2 = right to left.
	CB_GEM.data.ball.firstPlayer = Math.floor(Math.random() * 2) + 1; //1 or 2 (random).
	
	//Updates all visual elements according to the screen size:
	resizeElements(graphicSpritesSceneObject, true);

	//Shows the information for the first time:
	updateInfo(graphicSpritesSceneObject);
	
	//Prepares the sound effects and plays one of them (recommended to do this through a user-driven event):
	try
	{
		prepareSoundFx(); //Prepares sound effects to be used later.
		playSoundFx("start");
	}
	catch(E)
	{
		CB_console("Error preparing sounds or playing sound with 'start' ID: " + E);
		CB_GEM.data.soundEnabled = false; //If it fails, disables the sound.
	}
	
	CB_Device.Vibration.start(100); //Makes the device vibrate.
	
	//Starts playing the background music (recommended to do this through a user-driven event):
	playMusic(); //If it was playing already, it will not do anything.
	
	//Sets the game as started:
	CB_GEM.data.gameStarted = true; //When set to true, starts the game automatically as the game loops detect it.
}


//Ends the game:
function gameEnd(message)
{
	if (!CB_GEM.data.gameStarted) { return; }

	hideGoalMessage(); //Hides the "Goal!" message (just in case).
	
	message = CB_trim(message);
	CB_GEM.data.gameStarted = false;
	CB_Elements.insertContentById("start_button", (message !== "" ? message + "<br />" : "") + "Start game!")
	CB_Elements.showById("start_button"); //Shows the start button again.
}


//Updates the information shown:
var updateInfoDebug = false;
function updateInfo(graphicSpritesSceneObject)
{
	graphicSpritesSceneObject.getById("info").get(0).src =
		"Level: " + CB_GEM.data.level + "\n" +
		"Score: " + CB_GEM.data.player1.goals + " - " + CB_GEM.data.player2.goals + "\n" +
		"Pointer: x = " + pointerCoordinates.x + ", y = " + pointerCoordinates.y + "\n" +
		(!CB_Screen.isLandscape() ? "\nLandscape screen recommended!" : "") +
		(
			updateInfoDebug ?
				"Ball update speed (ms): " + CB_GEM.data.ball.speedMs +
				"\nBall: " + CB_GEM.data.ball.x + ", " + CB_GEM.data.ball.y + " (" + (CB_GEM.data.ball.direction === 0 ? "stuck" : "released: " + (CB_GEM.data.ball.direction === 1 ? "left to right" : "right to left")) + ")" +
				", vertical inclination (%): " + CB_GEM.data.ball.movePercentageVertical
			: ""
		);
}


//Resizes all visual elements according to the screen size:
var _fieldLeftPrevious = null;
var _fieldTopPrevious = null;
var _fieldWidthPrevious = null;
var _fieldHeightPrevious = null;
function resizeElements(graphicSpritesSceneObject, firstTime)
{
	if (graphicSpritesSceneObject instanceof CB_GraphicSpritesScene)
	{
		//Calculates the sized and position of the visual elements according to the screen size:
		var fieldWidth = CB_Screen.getWindowWidth() - 150;
		var fieldHeight = fieldWidth / 3;
		var decrementPixelsHeight = 150;
		while (fieldHeight > CB_Screen.getWindowHeight() - 120)
		{
			fieldWidth = CB_Screen.getWindowWidth() - decrementPixelsHeight;
			fieldHeight = fieldWidth / 3;
			decrementPixelsHeight += 1;
		}
		var fieldLeft = (CB_Screen.getWindowWidth() - fieldWidth) / 2;
		var fieldTop = (CB_Screen.getWindowHeight() - fieldHeight) / 2;
		var fieldLineWidth = fieldWidth / 200;
	
		//Updates the size and position of the field:
		graphicSpritesSceneObject.getById("field_group").getById("field").left = CB_GEM.data.field.left = fieldLeft;

		graphicSpritesSceneObject.getById("field_group").getById("field").top = CB_GEM.data.field.top = fieldTop;
		graphicSpritesSceneObject.getById("field_group").getById("field").width = CB_GEM.data.field.width = fieldWidth;
		graphicSpritesSceneObject.getById("field_group").getById("field").height = CB_GEM.data.field.height = fieldHeight;
		CB_GEM.data.field.fieldLineWidth = fieldLineWidth;
		
		graphicSpritesSceneObject.getById("field_group").getById("field").getById("field_middle").data.x1 = fieldWidth / 2;
		graphicSpritesSceneObject.getById("field_group").getById("field").getById("field_middle").data.y1 = fieldLineWidth / 2;
		graphicSpritesSceneObject.getById("field_group").getById("field").getById("field_middle").data.x2 = fieldWidth / 2;
		graphicSpritesSceneObject.getById("field_group").getById("field").getById("field_middle").data.y2 = fieldHeight - fieldLineWidth / 2 + 1;
		
		graphicSpritesSceneObject.getById("field_group").getById("field").data.lineWidth = fieldLineWidth;
		graphicSpritesSceneObject.getById("field_group").getById("field").getById("field_middle").data.lineWidth = fieldLineWidth;
		
		//Updates the size and position of the ball and paddles (players):
		graphicSpritesSceneObject.getById("ball_group").getById("ball").data.radius = CB_GEM.data.ball.radius = fieldWidth / 180;
		var paddleHeight = fieldHeight / 5;
		graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_1").width = graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_2").width =
			CB_GEM.data.player1.width = CB_GEM.data.player2.width = fieldWidth / 60;
		graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_1").height = graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_2").height =
			CB_GEM.data.player1.height = CB_GEM.data.player2.height = paddleHeight;
		graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_1").left = CB_GEM.data.player1.x = fieldLeft + fieldLineWidth / 2;
		graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_2").left = CB_GEM.data.player2.x = fieldLeft - fieldLineWidth / 2 + fieldWidth - graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_2").width;
		if (firstTime)
		{
			//Sets the position of the paddles:
			CB_GEM.data.player1.y = CB_GEM.data.player2.y =
				graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_1").top = graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_2").top =
				fieldTop + fieldLineWidth / 2 + fieldHeight / 2 - paddleHeight / 2;

			//Sets the position of the ball:
			graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x =
				CB_GEM.data.ball.firstPlayer === 1 ?
					fieldLeft + graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_1").width + CB_GEM.data.ball.radius + fieldLineWidth / 2 :
					fieldLeft + fieldWidth - graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_2").width - CB_GEM.data.ball.radius - fieldLineWidth / 2
			graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y =
				CB_GEM.data.player1.y + paddleHeight / 2;
		}
		else
		{
			//Sets the position of the paddles:
			CB_GEM.data.player1.y = graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_1").top =
				(CB_GEM.data.player1.y - _fieldTopPrevious) / _fieldHeightPrevious * fieldHeight + fieldTop;
			CB_GEM.data.player2.y = graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_2").top =
				(CB_GEM.data.player2.y - _fieldTopPrevious) / _fieldHeightPrevious * fieldHeight + fieldTop;
			
			//Sets the position of the ball:
			graphicSpritesSceneObject.getById("ball_group").getById("ball").left = CB_GEM.data.ball.x =
				(CB_GEM.data.ball.x - _fieldLeftPrevious) / _fieldWidthPrevious * fieldWidth + fieldLeft;
			graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y =
				(CB_GEM.data.ball.y - _fieldTopPrevious) / _fieldHeightPrevious * fieldHeight + fieldTop;
		}
		
		//Stores the current data of the field (for next times):
		_fieldLeftPrevious = fieldLeft;
		_fieldTopPrevious = fieldTop;
		_fieldWidthPrevious = fieldWidth;
		_fieldHeightPrevious = fieldHeight;
		
		//Resizes the message when a goal is scored:
		var fontSize = parseInt(Math.min(CB_Screen.getWindowWidth(), CB_Screen.getWindowHeight()) * 0.07) + "px";
		var goalElement = CB_Elements.id("goal");
		if (goalElement !== null)
		{
			goalElement.style.fontSize = goalElement.style.lineHeight = parseInt(fontSize) * 1.8 + "px";
			goalElement.style.left = (CB_Screen.getWindowWidth() / 2 - parseInt(goalElement.style.fontSize) * 8 / 6) + "px";
			goalElement.style.top = (CB_Screen.getWindowHeight() / 2 - parseInt(goalElement.style.fontSize) / 2) + "px";
		}
		
		//Resizes the FPS and the information text:
		CB_GEM.graphicSpritesSceneObject.getById("fps_group").getById("fps").data.fontSize = parseInt(fontSize) / 1.5 + "px";
		CB_GEM.graphicSpritesSceneObject.getById("info").get(0).top = parseInt(fontSize) / 2;
		CB_GEM.graphicSpritesSceneObject.getById("info").get(0).data.fontSize = parseInt(fontSize) / 2 + "px";
		
		//Resizes the font of the start button:
		var startButton = CB_Elements.id("start_button");
		if (startButton !== null) { startButton.style.fontSize = parseInt(fontSize) / 3.5 + "px"; }
	}
}


//Defines the events to detect the pointer input and capture the coordinates:
var pointerCoordinates = { x: null, y: null };
function setPointerEvents()
{
	//Gets and stores or removes the different touches (there can be more than one simultaneously):
	var touchesGet = function(e)
	{
		if (!e.touches) { return; }
		
		//Gets the touching points:
		var touchesCoordinates = {};
		var touchId = null;
		
		for (var x = e.touches.length - 1; x >= 0; x--)
		{
			touchId = typeof(e.touches[x].identifier) !== "undefined" ? e.touches[x].identifier : x;
			touchesCoordinates[touchId] = {};

			if (!pointerCoordinates.touchesCoordinates) { pointerCoordinates.touchesCoordinates = {}; }
			else if (pointerCoordinates.touchesCoordinates[touchId])
			{
				touchesCoordinates[touchId].xPrevious = pointerCoordinates.touchesCoordinates[touchId].x !== e.touches[x].clientX ? pointerCoordinates.touchesCoordinates[touchId].x : pointerCoordinates.touchesCoordinates[touchId].xPrevious;
				touchesCoordinates[touchId].yPrevious = pointerCoordinates.touchesCoordinates[touchId].y !== e.touches[x].clientY ? pointerCoordinates.touchesCoordinates[touchId].y : pointerCoordinates.touchesCoordinates[touchId].yPrevious;
			}

			touchesCoordinates[touchId].id = e.touches[x].identifier;
			touchesCoordinates[touchId].x = e.touches[x].clientX;
			touchesCoordinates[touchId].y = e.touches[x].clientY;
			touchesCoordinates[touchId].processed = false;

			pointerCoordinates.touchesCoordinates[touchId] = touchesCoordinates[touchId];
		}
		e.preventDefault(); //Prevents default behaviour (it will be polyfilled internally if needed, automatically).
	}
	CB_Touch.onStart(touchesGet);
	CB_Touch.onEnter(touchesGet);
	CB_Touch.onMove(touchesGet);
	var touchesClear = function(e) //Clears the touches coordinates:
	{
		//Only keeps the remaining touches:
		if (pointerCoordinates.touchesCoordinates)
		{
			var touchesCoordinates = {};
			for (var x = e.touches.length - 1; x >= 0; x--)
			{
				touchId = typeof(e.touches[x].identifier) !== "undefined" ? e.touches[x].identifier : x;
				touchesCoordinates[touchId] = {};
				touchesCoordinates[touchId].id = e.touches[x].identifier;
				touchesCoordinates[touchId].x = e.touches[x].clientX;
				touchesCoordinates[touchId].y = e.touches[x].clientY;
				touchesCoordinates[touchId].processed = pointerCoordinates.touchesCoordinates[touchId] ? pointerCoordinates.touchesCoordinates[touchId].processed : false;
			}
			pointerCoordinates.touchesCoordinates = touchesCoordinates;
		}
	}
	CB_Touch.onEnd(touchesClear);
	CB_Touch.onCancel(touchesClear);
	CB_Touch.onLeave(touchesClear);
	
	//Gets and stores or removes the pointer coordinates (only uses one touching point):
	var pointerCoordinatesGet = function(e) //Gets the pointer coordinates:
	{
		pointerCoordinates.type = e.typeEmulated || e.type;
		pointerCoordinates.isMouseOrPointer = pointerCoordinates.type === "mousedown" || pointerCoordinates.type === "mousemove" || pointerCoordinates.type === "pointerdown" || pointerCoordinates.type === "pointermove";
		pointerCoordinates.xPrevious = pointerCoordinates.x;
		pointerCoordinates.yPrevious = pointerCoordinates.y;
		pointerCoordinates.x = e.clientX;
		pointerCoordinates.y = e.clientY;
		pointerCoordinates.processed = false;
		if (pointerCoordinates.type !== "pointermove") //Only modifies (enables or disables it) the pressing flag ("down" property) when the pointer is not moving:
		{
			pointerCoordinates.down = (pointerCoordinates.type === "pointerdown" || pointerCoordinates.type === "pointerenter"); //Defines whether the pointer is being pressing or not (note that this way it will never be true when emulating Pointer events).
		}
		e.preventDefault(); //Prevents default behaviour (it will be polyfilled internally if needed).
	}; 

	CB_Pointer.onDown(pointerCoordinatesGet);
	CB_Pointer.onEnter(pointerCoordinatesGet);
	CB_Pointer.onMove(pointerCoordinatesGet); //Gets the pointer coordinates.
	var pointerCoordinatesClear = function (e) //Clears the pointer coordinates:
	{
		pointerCoordinates.x = pointerCoordinates.y = null;
		pointerCoordinates.down = null;
	};
	CB_Pointer.onUp(pointerCoordinatesClear);
	CB_Pointer.onCancel(pointerCoordinatesClear);
	CB_Pointer.onLeave(pointerCoordinatesClear);
	CB_Pointer.onOut(pointerCoordinatesClear);
}


//Input management (some controllers can also fire keyboard events):
function manageInput()
{
	if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.ESC) || CB_Controllers.isButtonDown([5, 6, 7, 8])) { gameEnd("Game aborted"); } //After pressing the ESC key, ends the game.
	else
	{
		//If return, space or a button (button 1, 2 or 3) or axis from any gamepad is pressed, starts the game (if it has not stared already):
		if (!CB_GEM.data.gameStarted && (CB_Keyboard.isKeyDown(CB_Keyboard.keys.ENTER) || CB_Keyboard.isKeyDown(CB_Keyboard.keys.SPACEBAR) || CB_Controllers.isButtonDown([1, 2, 3]) || CB_Controllers.getAxesDown().length > 0 || CB_Controllers.getAxesDown("", -1).length > 0))
		{
			gameStart(CB_GEM.graphicSpritesSceneObject);
			return;
		}
		
		var touchingScreen = false;
		
		//Player 1 (uses gamepad #1):
		if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.UP) || CB_Controllers.isAxisDown(1, 1, -1)) { movePaddle(1, 0, CB_GEM.graphicSpritesSceneObject); } //Moves up.
		if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.DOWN) || CB_Controllers.isAxisDown(1, 1)) { movePaddle(1, 1, CB_GEM.graphicSpritesSceneObject); } //Moves down.

		//Player 2 (uses gamepad #0):
		if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.Q) || CB_Controllers.isAxisDown(1, 0, -1)) { movePaddle(2, 0, CB_GEM.graphicSpritesSceneObject); } //Moves up.
		if (CB_Keyboard.isKeyDown(CB_Keyboard.keys.A) || CB_Controllers.isAxisDown(1, 0)) { movePaddle(2, 1, CB_GEM.graphicSpritesSceneObject); } //Moves down.
		
		//Mouse (if there is at least the coordinates of one point):
		if (pointerCoordinates.isMouseOrPointer && pointerCoordinates.y !== null && pointerCoordinates.x !== null && !pointerCoordinates.processed) //Only if the information has been updated but it has not been processed yet.
		{
			var playerToMove = pointerCoordinates.x < CB_Screen.getWindowWidth() / 2 ? 1 : 2; //The player to move (1 or 2) will depend on the part of the screen touched.
			
			//Player 1 or 2, depending on the part of the screen touched (uses pointer which can be mouse or stylus/touch):
			movePaddleToPoint(playerToMove, pointerCoordinates.y - CB_GEM.data.player1.height / 2, CB_GEM.graphicSpritesSceneObject); //Moves up or down to the desired coordinates (centering the mouse coordinates in the middle of the paddle).
				
			pointerCoordinates.processed = true;
		}
		//Stylus/Touch:
		else if (pointerCoordinates.touchesCoordinates)// && !pointerCoordinates.processed) //Only if the information has been updated but it has not been processed yet.
		{
			var touchesProcessed = 0;
			var playerToMove = null;
			for (var touchId in pointerCoordinates.touchesCoordinates)
			{
				if (touchesProcessed >= 2) { break; }
				else if (!pointerCoordinates.touchesCoordinates[touchId]) { continue; } //Erased point.
				else if (pointerCoordinates.touchesCoordinates[touchId].processed) { touchesProcessed++; continue; } //Point already processed.
				else if (typeof(pointerCoordinates.touchesCoordinates[touchId].yPrevious) !== "undefined" && pointerCoordinates.touchesCoordinates[touchId].y !== pointerCoordinates.touchesCoordinates[touchId].yPrevious)
				{
					//Player 1 or 2, depending on the part of the screen touched (uses pointer which can be mouse or stylus/touch):
					playerToMove = pointerCoordinates.touchesCoordinates[touchId].x < CB_Screen.getWindowWidth() / 2 ? 1 : 2; //The player to move (1 or 2) will depend on the part of the screen touched.
					if (pointerCoordinates.touchesCoordinates[touchId].y < pointerCoordinates.touchesCoordinates[touchId].yPrevious) { movePaddle(playerToMove, 0, CB_GEM.graphicSpritesSceneObject, _paddlePixelsMoveTouchDefault); } //Moves up.
					else { movePaddle(playerToMove, 1, CB_GEM.graphicSpritesSceneObject, _paddlePixelsMoveTouchDefault); } //Moves down.
					
					pointerCoordinates.touchesCoordinates[touchId].processed = true;
					touchesProcessed++;
				}
			}
			if (touchesProcessed > 0) { pointerCoordinates.processed = true; touchingScreen = true; }
		}
		
		//Releases the ball if it was stuck on a paddle (and the game started):
		if (CB_GEM.data.gameStarted && CB_GEM.data.ball.direction === 0) //0 = not moving (stuck to a paddle), 1 = left to right, 2 = right to left.
		{
			//Player 1 (uses gamepad #1):
			if (CB_GEM.data.ball.firstPlayer === 1 && (touchingScreen || pointerCoordinates.down || CB_Mouse.getButtons().LEFT || CB_Keyboard.isKeyDown(CB_Keyboard.keys.ENTER) || CB_Keyboard.isKeyDown(CB_Keyboard.keys.SPACEBAR) || CB_Controllers.isButtonDown([1, 2, 3], 1)))
			{
				//Releases the ball:
				CB_GEM.data.ball.direction = 1; //0 = not moving (stuck to a paddle), 1 = left to right, 2 = right to left.
				updateInfo(CB_GEM.graphicSpritesSceneObject); //Updates the information shown.
				playSoundFx("release_ball"); //Plays the release sound.
			}
			//Player 2 (uses gamepad #0):
			else if (CB_GEM.data.ball.firstPlayer === 2 && (touchingScreen || pointerCoordinates.down || CB_Mouse.getButtons().RIGHT || CB_Keyboard.isKeyDown(CB_Keyboard.keys.SPACEBAR) || CB_Controllers.isButtonDown([1, 2, 3], 0)))
			{
				//Releases the ball:
				CB_GEM.data.ball.direction = 2; //0 = not moving (stuck to a paddle), 1 = left to right, 2 = right to left.
				updateInfo(CB_GEM.graphicSpritesSceneObject); //Updates the information shown.
				playSoundFx("release_ball"); //Plays the release sound.
			}
		}
	}
}


//Moves the desired paddle to the given direction:
var _paddlePixelsMoveDefault = 10;
var _paddlePixelsMoveTouchDefault = 5;
function movePaddle(paddleId, direction, graphicSpritesSceneObject, paddlePixelsMove) //The direction set to 0 (zero) is for going up.
{
	if (!CB_GEM.data.gameStarted) { return null; }
	
	if (typeof(paddlePixelsMove) === "undefined" || paddlePixelsMove === null || isNaN(paddlePixelsMove)) { paddlePixelsMove = _paddlePixelsMoveDefault; }
	
	var paddleTop = CB_GEM.data["player" + paddleId].y;
	var fieldLineWidth = CB_GEM.data.field.width / 200;
	
	if (direction === 0)
	{
		paddleTop -= paddlePixelsMove;
		if (paddleTop < CB_GEM.data.field.top + fieldLineWidth / 2) { paddleTop = CB_GEM.data.field.top + fieldLineWidth / 2; }
	}
	else
	{
		paddleTop += paddlePixelsMove;
		var paddleHeight = CB_GEM.data.field.height / 5;
		if (paddleTop > CB_GEM.data.field.top + CB_GEM.data.field.height - paddleHeight - fieldLineWidth / 2) { paddleTop = CB_GEM.data.field.top + CB_GEM.data.field.height - paddleHeight - fieldLineWidth / 2; }
	}
	
	//If there has not been any change, returns:
	if (paddleTop === CB_GEM.data["player" + paddleId].y) { return null; }
	
	//Moves the paddle:
	CB_GEM.data["player" + paddleId].y = graphicSpritesSceneObject.getById("paddles_group").getById("paddles").getById("paddle_" + paddleId).top = paddleTop;
	
	//If the ball is stuck to the paddle which is moving, also moves the ball:
	if (CB_GEM.data.ball.direction === 0 && CB_GEM.data.ball.firstPlayer === paddleId) //0 = not moving (stuck to a paddle), 1 = left to right, 2 = right to left.
	{
		//var paddleHeight = CB_GEM.data.field.height / 5;
		//graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data["player" + paddleId].y + paddleHeight / 2;
		graphicSpritesSceneObject.getById("ball_group").getById("ball").top = CB_GEM.data.ball.y = CB_GEM.data["player" + paddleId].y + CB_GEM.data["player" + paddleId].height / 2;
	}
	
	return paddleTop;
}


//Returns the desired gradient style:
function getGradientStyle(firstColour, lastColour, element, canvasContext)
{
	var gradient = canvasContext.createLinearGradient(element.left, element.top, element.left + element.width, element.top + element.height);
	gradient.addColorStop(0, firstColour);
	gradient.addColorStop(1, lastColour);
	return gradient;
}


//Moves the desired paddle to the desired point:
function movePaddleToPoint(paddleId, yDestiny, graphicSpritesSceneObject)
{
	var paddleTop = CB_GEM.data["player" + paddleId].y;
	var direction = paddleTop < yDestiny ? 1: 0;
	
	while (paddleTop !== null && Math.abs(paddleTop - yDestiny) >= _paddlePixelsMoveDefault)
	{
		paddleTop = movePaddle(paddleId, direction, graphicSpritesSceneObject);
	}
	
	return paddleTop;
}


//Shows the "Goal!" message:
var showingGoalMessage = false;
var showGoalMessageTimeout = null;
function showGoalMessage()
{
	clearTimeout(showGoalMessageTimeout);
	CB_Elements.showById("goal");
	showingGoalMessage = true;
	showGoalMessageTimeout = setTimeout(hideGoalMessage, 1000);
}


//Hides the "Goal!" message:
function hideGoalMessage()
{
	clearTimeout(showGoalMessageTimeout);
	CB_Elements.hideById("goal");
	showingGoalMessage = false;
}


//Prepares sound effects:
var sfx = null; //Global object to play the sounds.
var prepareSoundFxExecuted = false;
function prepareSoundFx()
{
	if (prepareSoundFxExecuted) { return; }

	prepareSoundFxExecuted = true;
	
	var jsfxObject = CB_Speaker.getJsfxObject(); //Gets the 'jsfx' object.
	if (jsfxObject !== null)
	{
		//Defines the sound effects:
		var library =
		{
			"start":
				jsfx.Preset.Coin,
			"hit_border":
				jsfx.Preset.Select,
			"hit_paddle":
				jsfx.Preset.Explosion,
			"goal":
				jsfx.Preset.Jump,
			"release_ball":
				jsfx.Preset.Reset
		};

		//Loads the sound effects:
		sfx = CB_AudioDetector.isAPISupported("WAAPI") ? jsfxObject.Live(library) : jsfxObject.Sounds(library); //Uses AudioContext (Web Audio API) if available.
	}
}


//Plays the desired sound effect (by its identifier):
function playSoundFx(id)
{
	if (!sfx || typeof(sfx[id]) !== "function") { return; }
	else if (!CB_GEM.data.soundEnabled) { return; }

	//Note: at least the first time, it is recommended to do it through a user-driven event (as "onClick", "onTouchStart", etc.) in order to maximize compatibility (as some clients could block sounds otherwise).
	sfx[id]();
}


//Prepares background music:
var bandJSObject = null; //Global 'BandJS' object to play the sounds.
function prepareMusic()
{
	bandJSObject = CB_Speaker.getBandJSObject(); //Gets a new 'BandJS' object.
	if (bandJSObject !== null)
	{
		//Sets a time signature:
		bandJSObject.setTimeSignature(4, 4);

		//Sets the tempo:
		bandJSObject.setTempo(80);

		//Shows the controls:
		CB_Elements.showById("controls");
	}
}


//Plays the background music:
//* Frog's Theme (Chrono Trigger) by Yasunori Mitsuda (version by Jarred Mack and Cody Lundquist using Band.js library). Taken from: https://github.com/meenie/band.js (example: http://plnkr.co/edit/vVrFxg?p=preview).
var playingMusic = false;
function playMusic()
{
	if (bandJSObject === null || playingMusic) { return; }
	else if (!CB_GEM.data.soundEnabled) { return; }

	playingMusic = true;
	
	//Creates an instrument and adds notes:
	var tempo = 126;
	bandJSObject.setTimeSignature(4, 4);
    bandJSObject.setTempo(tempo);

    var right = bandJSObject.createInstrument('square', 'oscillators');
	var left = bandJSObject.createInstrument('square', 'oscillators');
	
	right.setVolume(0.50);
	left.setVolume(0.50);

    right.note('tripletEighth', 'A4')
    .note('tripletEighth', 'G4')
    .note('tripletEighth', 'A4')
    .note('quarter', 'E4')
    .note('quarter', 'A4')
    .note('tripletQuarter', 'B4')
    .note('tripletEighth', 'C5');
    
    left.note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3');
    
    right.note('tripletEighth', 'B4')
    .note('tripletEighth', 'A4')
    .note('tripletEighth', 'B4')
    .note('half', 'G4')
    .note('quarter', 'D4');
    
    left.note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3');
    
    right.note('tripletEighth', 'A4')
    .note('tripletEighth', 'G4')
    .note('tripletEighth', 'A4')
    .note('quarter', 'E4')
    .note('quarter', 'A4')
    .note('tripletQuarter', 'B4')
    .note('tripletEighth', 'C5');
    
    left.note('quarter', 'A3, F3')
    .note('quarter', 'A3, F3')
    .note('quarter', 'A3, F3')
    .note('quarter', 'A3, F3');
    
    right.note('tripletEighth', 'B4')
    .note('tripletEighth', 'C5')
    .note('tripletEighth', 'D5')
    .note('dottedHalf', 'B4');
    
    left.note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3');
    
    right.note('tripletEighth', 'A4')
    .note('tripletEighth', 'G4')
    .note('tripletEighth', 'A4')
    .note('quarter', 'E4')
    .note('quarter', 'A4')
    .note('tripletQuarter', 'B4')
    .note('tripletEighth', 'C5');
    
    left.note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3');
    
    right.note('tripletEighth', 'B4')
    .note('tripletEighth', 'A4')
    .note('tripletEighth', 'B4')
    .note('half', 'G4')
    .note('quarter', 'D4');
    
    left.note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3');
    
    right.note('tripletEighth', 'A4')
    .note('tripletEighth', 'G4')
    .note('tripletEighth', 'A4')
    .note('quarter', 'B4')
    .note('quarter', 'C5')
    .note('quarter', 'B4');
    
    left.note('quarter', 'A3, F3')
    .note('quarter', 'A3, F3')
    .note('quarter', 'B3, G3')
    .note('quarter', 'B3, G3');
    
    right.note('tripletEighth', 'A4')
    .note('tripletEighth', 'G4')
    .note('tripletEighth', 'A4')
    .note('tripletEighth', 'A4')
    .note('tripletEighth', 'G4')
    .note('tripletEighth', 'A4')
    .note('quarter', 'A4')
    .rest('quarter');
    
    left.note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3')
    .note('quarter', 'C4, A3')
    .rest('quarter');
    
    right.note('tripletQuarter', 'E5')
    .note('tripletEighth', 'E5')
    .note('tripletEighth', 'E5')
    .note('tripletEighth', 'C5')
    .note('tripletEighth', 'D5')
    .note('quarter', 'E5')
    .note('quarter', 'A5');
    
    left.note('quarter', 'E4, C4, A3')
    .note('quarter', 'E4, C4, A3')
    .note('quarter', 'E4, C4, A3')
    .note('quarter', 'E4, C4, A3');
    
    right.note('half', 'G5')
    .note('dottedQuarter', 'E5')
    .note('eighth', 'C5');
    
    left.note('quarter', 'C4, G3, E3')
    .note('quarter', 'C4, G3, E3')
    .note('quarter', 'C4, G3, E3')
    .note('quarter', 'C4, G3, E3');
    
    right.note('dottedHalf', 'A4')
    .rest('quarter');
    
    left.note('quarter', 'C4, A3, F3')
    .note('quarter', 'C4, A3, F3')
    .note('quarter', 'C4, A3, F3')
    .note('quarter', 'C4, A3, F3');
    
    right.note('tripletEighth', 'A4')
    .note('tripletEighth', 'B4')
    .note('tripletEighth', 'C5')
    .note('tripletEighth', 'B4')
    .note('tripletEighth', 'C5')
    .note('tripletEighth', 'D5')
    .note('tripletEighth', 'C5')
    .note('tripletEighth', 'D5')
    .note('tripletEighth', 'E5')
    .note('tripletEighth', 'D5')
    .note('tripletEighth', 'E5')
    .note('tripletEighth', 'G5');
    
    left.note('quarter', 'D4, B3, G3')
    .note('quarter', 'D4, B3, G3')
    .rest('tripletEighth')
    .note('tripletEighth', 'D4, B3, G3')
    .rest('tripletEighth')
    .note('quarter', 'D4, B3, G3');
    
    right.note('tripletQuarter', 'E5')
    .note('tripletEighth', 'E5')
    .note('tripletEighth', 'E5')
    .note('tripletEighth', 'C5')
    .note('tripletEighth', 'D5')
    .note('quarter', 'E5')
    .note('quarter', 'A5');
    
    left.note('quarter', 'E4, C4, A3')
    .note('quarter', 'E4, C4, A3')
    .note('quarter', 'E4, C4, A3')
    .note('quarter', 'E4, C4, A3');
    
    right.note('quarter', 'B5', true)
    .note('tripletEighth', 'B5')
    .note('tripletEighth', 'C6')
    .note('tripletEighth', 'B5')
    .note('quarter', 'A5')
    .note('quarter', 'G5');
    
    left.note('quarter', 'D4, B3, G3')
    .note('quarter', 'D4, B3, G3')
    .note('quarter', 'D4, B3, G3')
    .note('quarter', 'D4, B3, G3');
    
    right.note('whole', 'A5');
    
    left.note('quarter', 'C4, A3, F3')
    .note('quarter', 'C4, A3, F3')
    .note('quarter', 'C4, A3, F3')
    .note('quarter', 'C4, A3, F3');
    
    right.note('half', 'A5')
    .note('half', 'B5');
    
    left.note('quarter', 'B3, A3, E3')
    .note('quarter', 'B3, A3, E3')
    .rest('tripletEighth')
    .note('tripletEighth', 'B3, G#3, E3')
    .rest('tripletEighth')
    .note('quarter', 'B3, G#3, E3');
    
    right.note('tripletQuarter', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'B5')
      .note('tripletEighth', 'A5')
      .note('quarter', 'G5')
      .note('quarter', 'E5');
      
    left.note('quarter', 'F3')
      .note('quarter', 'C4, A3, F3')
      .note('quarter', 'G3')
      .note('quarter', 'D4, B3, G3');
      
    right.note('tripletQuarter', 'A5')
      .note('tripletEighth', 'A5')
      .note('tripletEighth', 'A5')
      .note('tripletEighth', 'G5')
      .note('tripletEighth', 'F5')
      .note('half', 'E5');
      
    left.note('quarter', 'F3')
      .note('quarter', 'C4, A3, F3')
      .note('quarter', 'A3')
      .note('quarter', 'E4, B3, A3');
      
    right.note('tripletQuarter', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'B5')
      .note('tripletEighth', 'A5')
      .note('quarter', 'G5')
      .note('quarter', 'E5');
      
      
    left.note('quarter', 'F3')
      .note('quarter', 'C4, A3, F3')
      .note('quarter', 'G3')
      .note('quarter', 'D4, B3, G3');
      
    right.note('half', 'A5')
      .note('half', 'B5');
      
    left.note('quarter', 'A3, F#3, D3')
      .note('tripletEighth', 'A3, F#3, D3')
      .note('tripletEighth', 'A3, F#3, D3')
      .note('tripletEighth', 'A3, F#3, D3')
      .note('quarter', 'B3, G#3, E3')
      .note('tripletEighth', 'B3, G#3, E3')
      .note('tripletEighth', 'B3, G#3, E3')
      .note('tripletEighth', 'B3, G#3, E3');
      
    right.note('tripletQuarter', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'B5')
      .note('tripletEighth', 'A5')
      .note('quarter', 'G5')
      .note('quarter', 'E5');
      
    left.note('quarter', 'F3')
      .note('quarter', 'C4, A3, F3')
      .note('quarter', 'G3')
      .note('quarter', 'D4, B3, G3');
      
    right.note('tripletQuarter', 'A5')
      .note('tripletEighth', 'A5')
      .note('tripletEighth', 'A5')
      .note('tripletEighth', 'G5')
      .note('tripletEighth', 'F5')
      .note('half', 'E5');
      
    left.note('quarter', 'F3')
      .note('quarter', 'C4, A3, F3')
      .note('quarter', 'A3')
      .note('quarter', 'E4, B3, A3');
      
    right.note('tripletQuarter', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'C6')
      .note('tripletEighth', 'B5')
      .note('tripletEighth', 'C6')
      .note('quarter', 'D6')
      .note('quarter', 'B5');
      
    left.note('quarter', 'F3')
      .note('quarter', 'C4, A3, F3')
      .note('quarter', 'G3')
      .note('quarter', 'D4, B3, G3');
      
    right.note('half', 'B5')
      .note('quarter', 'A5')
      .rest('quarter');
      
    left.note('quarter', 'E4, C4, A3')
      .note('tripletEighth', 'E4, C4, A3')
      .note('tripletEighth', 'E4, C4, A3')
      .note('tripletEighth', 'E4, C4, A3')
      .note('quarter', 'E4, C4, A3')
      .rest('quarter');
	
	bandJSObject.setMasterVolume(0.25);
	
	//Finishes and plays it:
	//Note: at least the first time, it is recommended to do it through a user-driven event (as "onClick", "onTouchStart", etc.) in order to maximize compatibility (as some clients could block sounds otherwise).
	var player = bandJSObject.finish();
	player.loop(true); //The music will loop constantly.
	player.play()
}

Try this example

You can check the Guides & Tutorials category as well as the API documentation in the case you need more information.

All the examples together can be downloaded here.

Go back to Guides & Tutorials

Try this example












Share