diff --git a/TennisGame/fonts/TitilliumWeb-Regular.ttf b/TennisGame/fonts/TitilliumWeb-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..6da821935dc29725c445ee8b469a2242c508c481
Binary files /dev/null and b/TennisGame/fonts/TitilliumWeb-Regular.ttf differ
diff --git a/TennisGame/game.uia b/TennisGame/game.uia
new file mode 100644
index 0000000000000000000000000000000000000000..01d364a5cb18aab0085e92a0c0befe03e240aab4
--- /dev/null
+++ b/TennisGame/game.uia
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<application xmlns="http://qt.io/qt3dstudio/uia">
+	<assets initial="game">
+		<presentation id="game"	src="game.uip"/>
+	</assets>
+
+	<statemachine ref="#logic">
+		<visual-states>
+			<state ref="Initial">
+				<enter>
+					<goto-slide element="main:Scene" rel="next"/>
+				</enter>
+			</state>
+		</visual-states>
+	</statemachine>
+</application>
diff --git a/TennisGame/game.uip b/TennisGame/game.uip
new file mode 100644
index 0000000000000000000000000000000000000000..ae8c4c628eca9a9d6994b736e5c0ff2dabcd7986
--- /dev/null
+++ b/TennisGame/game.uip
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<UIP version="3" >
+	<Project >
+		<ProjectSettings author="" company="" presentationWidth="800" presentationHeight="480" maintainAspect="False" >
+			<CustomColors count="16" >#ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff #ffffff</CustomColors>
+		</ProjectSettings>
+		<Classes >
+			<Behavior id="Movement" name="Movement" sourcepath="scripts\Movement.qml" />
+			<Behavior id="Score_001" name="Score" sourcepath="scripts\Score.qml" />
+		</Classes>
+		<Graph >
+			<Scene id="Scene" >
+				<Layer id="Layer" >
+					<Camera id="Camera" />
+					<Light id="Light" />
+					<Model id="Player" >
+						<Material id="Material" >
+							<Image id="Material_diffusemap" />
+						</Material>
+						<Behavior id="Movement_001" class="#Movement" />
+					</Model>
+					<Model id="Computer" >
+						<Material id="Material_002" >
+							<Image id="Material_002_diffusemap" />
+						</Material>
+						<Behavior id="Movement_003" class="#Movement" />
+					</Model>
+					<Model id="Ball" >
+						<Material id="Material_001" >
+							<Image id="Material_001_diffusemap" />
+						</Material>
+						<Behavior id="Movement_002" class="#Movement" />
+					</Model>
+					<Model id="Cube" >
+						<Material id="Material_003" >
+							<Image id="Material_003_diffusemap" />
+						</Material>
+					</Model>
+					<Model id="Cube2" >
+						<Material id="Material_004" >
+							<Image id="Material_004_diffusemap" />
+						</Material>
+					</Model>
+					<Text id="Score" >
+						<Behavior id="Score_002" class="#Score_001" />
+					</Text>
+					<Model id="Cube_001" >
+						<Material id="Material_005" >
+							<Image id="Material_005_diffusemap" />
+						</Material>
+					</Model>
+					<Text id="Text" />
+					<Text id="Text_001" />
+				</Layer>
+			</Scene>
+		</Graph>
+		<Logic >
+			<State name="Master Slide" component="#Scene" >
+				<Add ref="#Layer" aostrength="0" background="Transparent" blendtype="Normal" disabledepthprepass="False" disabledepthtest="False" height="480" heightunits="pixels" horzfields="Left/Width" left="0" leftunits="percent" progressiveaa="None" sourcepath="" temporalaa="False" width="800" widthunits="pixels" />
+				<Add ref="#Camera" orthographic="False" />
+				<Add ref="#Light" brightness="100" castshadow="True" lightambient="0.266667 0.266667 0.266667" lighttype="Point" position="0 0 -200" rotation="0 0 0" shdwfactor="73.3938" shdwfilter="15.85" shdwmapfov="90" shdwmapres="8" />
+				<State id="Scene-Game" name="Game" playmode="PingPong" >
+					<Set ref="#Scene" >
+						<Action id="Scene-Action" eyeball="True" triggerObject="#Scene" event="onPressureDown" targetObject="#Movement_001" handler="onMouseDown" />
+						<Action id="Scene-Action_001" eyeball="True" triggerObject="#Scene" event="onPressureUp" targetObject="#Movement_001" handler="onMouseUp" />
+					</Set>
+					<Set ref="#Layer" endtime="4000" />
+					<Set ref="#Camera" endtime="4000" />
+					<Set ref="#Light" castshadow="True" endtime="4000" />
+					<Add ref="#Player" name="Player" endtime="4000" position="-400 0 0" scale="0.5 1 0.5" sourcepath="#Cube" />
+					<Add ref="#Material" diffusemap="#Material_diffusemap" emissivecolor="1 1 1" emissivepower="0" />
+					<Add ref="#Material_diffusemap" sourcepath="maps\Wood7.png" subpresentation="" />
+					<Add ref="#Movement_001" name="Movement" endtime="4000" obstacle1="#Ball" speed="300" />
+					<Add ref="#Computer" name="Computer" endtime="4000" position="400 0 0" scale="0.5 1 0.5" sourcepath="#Cube" />
+					<Add ref="#Material_002" diffusemap="#Material_002_diffusemap" />
+					<Add ref="#Material_002_diffusemap" sourcepath="maps\Wood7.png" />
+					<Add ref="#Movement_003" name="Movement" endtime="4000" hasAI="True" obstacle1="#Ball" speed="300" />
+					<Add ref="#Ball" name="Ball" endtime="4000" position="0 0 0" scale="0.5 0.5 0.5" sourcepath="#Sphere" />
+					<Add ref="#Material_001" diffusemap="#Material_001_diffusemap" />
+					<Add ref="#Material_001_diffusemap" sourcepath="maps\Wood7.png" />
+					<Add ref="#Movement_002" name="Movement" endtime="4000" hasPhysics="True" obstacle1="#Player" obstacle2="#Computer" >
+						<Action id="Movement-Action" eyeball="True" triggerObject="#Movement_002" event="onPlayerScore" targetObject="#Score_002" handler="addPlayerScore" />
+						<Action id="Movement-Action_001" eyeball="True" triggerObject="#Movement_002" event="onComputerScore" targetObject="#Score_002" handler="addComputerScore" />
+					</Add>
+					<Add ref="#Cube" name="WallTop" endtime="4000" position="-2.59808 327.598 -2.59808" scale="20 0.1 1" sourcepath="#Cube" />
+					<Add ref="#Material_003" diffusemap="#Material_003_diffusemap" />
+					<Add ref="#Material_003_diffusemap" sourcepath="maps\Wood7.png" />
+					<Add ref="#Cube2" name="WallBottom" endtime="4000" position="0 -325 0" scale="20 0.1 1" sourcepath="#Cube" />
+					<Add ref="#Material_004" diffusemap="#Material_004_diffusemap" />
+					<Add ref="#Material_004_diffusemap" sourcepath="maps\Wood7.png" />
+					<Add ref="#Score" name="Score" endtime="4000" font="TitilliumWeb-Regular" leading="0" position="0 250 0" rotation="0 0 0" scale="1 1 1" size="72" textstring="0 - 0" tracking="0" />
+					<Add ref="#Score_002" name="Score" endtime="4000" >
+						<Action id="Score-Action" eyeball="True" triggerObject="#Score_002" event="onPlayerWins" targetObject="#Scene" handler="Go to Slide" >
+							<HandlerArgument name="Slide" type="String" argtype="Slide" value="Win" />
+						</Action>
+						<Action id="Score-Action_001" eyeball="True" triggerObject="#Score_002" event="onComputerWins" targetObject="#Scene" handler="Go to Slide" >
+							<HandlerArgument name="Slide" type="String" argtype="Slide" value="Lose" />
+						</Action>
+					</Add>
+					<Add ref="#Cube_001" name="Wall" endtime="4000" position="0 0 75" scale="20 10 1" sourcepath="#Cube" />
+					<Add ref="#Material_005" diffusemap="#Material_005_diffusemap" />
+					<Add ref="#Material_005_diffusemap" scaleu="4" scalev="2" sourcepath="maps\Cork1.png" tilingmodehorz="Tiled" tilingmodevert="Tiled" />
+				</State>
+				<State id="Scene-Win" name="Win" initialplaystate="Play" playmode="Stop at end" playthroughto="Previous" >
+					<Set ref="#Scene" >
+						<Action id="Scene-Action_002" eyeball="True" triggerObject="#Scene" event="onPressureDown" targetObject="#Scene" handler="Go to Slide" >
+							<HandlerArgument name="Slide" type="String" argtype="Slide" value="Game" />
+						</Action>
+					</Set>
+					<Set ref="#Layer" endtime="4000" />
+					<Set ref="#Camera" endtime="4000" />
+					<Set ref="#Light" castshadow="True" endtime="4000" />
+					<Add ref="#Text" name="Text" endtime="4000" font="TitilliumWeb-Regular" pivot="0 0 0" scale="1 1 1" size="72" textstring="You win!" />
+				</State>
+				<State id="Scene-Lose" name="Lose" playthroughto="Previous" >
+					<Set ref="#Scene" >
+						<Action id="Scene-Action_003" eyeball="True" triggerObject="#Scene" event="onPressureDown" targetObject="#Scene" handler="Go to Slide" >
+							<HandlerArgument name="Slide" type="String" argtype="Slide" value="Game" />
+						</Action>
+					</Set>
+					<Set ref="#Layer" endtime="4000" />
+					<Set ref="#Camera" endtime="4000" />
+					<Set ref="#Light" castshadow="True" endtime="4000" />
+					<Add ref="#Text_001" name="Text" endtime="4000" font="TitilliumWeb-Regular" pivot="0 0 0" scale="1 1 1" size="72" textstring="You lose!" />
+				</State>
+			</State>
+		</Logic>
+	</Project>
+</UIP>
diff --git a/TennisGame/maps/Cork1.png b/TennisGame/maps/Cork1.png
new file mode 100644
index 0000000000000000000000000000000000000000..b27b27d40ce9fe5146d8e49da42a68c9b5ec6384
Binary files /dev/null and b/TennisGame/maps/Cork1.png differ
diff --git a/TennisGame/maps/Wood7.png b/TennisGame/maps/Wood7.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d9a219a459b4176ba1a95d8897f6788c9604527
Binary files /dev/null and b/TennisGame/maps/Wood7.png differ
diff --git a/TennisGame/readme.md b/TennisGame/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..ff073e6a18950cff43582c0e9caf7b498d058ee7
--- /dev/null
+++ b/TennisGame/readme.md
@@ -0,0 +1,29 @@
+# Qt 3D Studio Tennis Game Demo
+
+This demo application is created with Qt 3D Studio.
+Behaviors and actions are used to implement a tennis game. The player is pitted against a computer opponent
+with a mission of getting the ball behind the opponent to score. Whoever gets 3 points first wins.
+The paddle of the player is controlled with a mouse by clicking on the screen and moving around.
+
+Two custom behaviors are used: Movement.qml and Score.qml.
+Movement.qml is the most complex script used. It handles the movement of the player, AI and the ball.
+It includes collision detection and handles score detection and ball serving also.
+
+Properties that can be set from the studio for Movement.qml are:
+- Obstacle 1: Object that can be collided with
+- Obstacle 2: Another object that can be collided with
+- Has Physics: Determines if the object is handled as a ball
+- Has AI: Determines if the object is handled as the AI paddle
+
+The important handlers for Movement.qml are "onMouseDown" and "onMouseUp".
+The Scene object in the presentation includes actions that listen for mouse input to the screen.
+If such events occur the functions "onMouseDown" and "onMouseUp" from Movement.qml are called.
+The script can then use the inputs to move the player around.
+
+Movement.qml can also output two events: "onPlayerScore" and "onComputerScore".
+The Movement script for the ball includes two actions in the studio that listen for these events.
+When one side scores the functions that increment scores in the Score script, owned by the score text object, are called.
+
+Score.qml itself also outputs two events: "onPlayerWins" and "onComputerWins".
+The Score scripts increments the scores and, if one score is equal to 3, fires the win event for the owner of that score.
+The actions that listen for the win events then tell the Scene object to jump to a victory or a defeat slide.
\ No newline at end of file
diff --git a/TennisGame/scripts/Movement.qml b/TennisGame/scripts/Movement.qml
new file mode 100644
index 0000000000000000000000000000000000000000..562658eb957ae73bc5e2f6abf79088e4bb641165
--- /dev/null
+++ b/TennisGame/scripts/Movement.qml
@@ -0,0 +1,221 @@
+/*[[
+<Property name="obstacle1" formalName="Obstacle 1" type="ObjectRef" />
+<Property name="obstacle2" formalName="Obstacle 2" type="ObjectRef" />
+<Property name="hasPhysics" formalName="Has Physics?" animatable="False" type="Boolean" default="False" />
+<Property name="hasAI" formalName="Has AI?" animatable="False" type="Boolean" default="False" />
+<Property name="speed" formalName="Speed" animatable="False" type="Float" default="300" />
+
+<Handler name="onMouseDown" formalName="On Mouse Down" />
+<Handler name="onMouseUp" formalName="On Mouse Up" />
+<Handler name="moveUp" formalName="Move Up" />
+<Handler name="moveDown" formalName="Move Down" />
+<Handler name="moveLeft" formalName="Move Left" />
+<Handler name="moveRight" formalName="Move Right" />
+<Handler name="stopHorizontal" formalName="Stop Horizontal" />
+<Handler name="stopVertical" formalName="Stop Vertical" />
+
+<Event name="onPlayerScore" />
+<Event name="onComputerScore" />
+]]*/
+
+import QtStudio3D.Behavior 1.0
+
+Behavior {
+    //Properties exposed to the studio
+    property var obstacle1
+    property var obstacle2
+    property var hasPhysics
+    property var hasAI
+    property var speed
+
+    //Properties used as global variables
+    property var mouseDown: false
+    property var input: Qt.vector3d(0, 0, 0);
+    property var direction: Qt.vector3d(-1, Math.random() * 2 - 1, 0);
+    property var physicsSpeed
+    property var serveTimer: 1
+
+    onInitialize: {
+        //Only the ball speed has to be reset during initalization
+        //since the ball movement speeds up during gameplay
+        physicsSpeed = speed;
+    }
+
+    onUpdate: {
+        //Pause the ball movement before the serve
+        if (hasPhysics && serveTimer > 0) {
+            serveTimer -= getDeltaTime();
+            return;
+        }
+
+        //Get the position of the script owner
+        var transform = calculateGlobalTransform();
+        var position = transform.row(3).toVector3d();
+
+        //Check if ball is out of bounds
+        var levelWidth = 600;
+        if (position.x > levelWidth)
+            fireEvent("onPlayerScore");
+        else if (position.x < -levelWidth)
+            fireEvent("onComputerScore");
+
+        if (position.x < -levelWidth || position.x > levelWidth) {
+            //Reset properties if out of bounds
+            setAttribute("position.x", 0);
+            setAttribute("position.y", 0);
+            transform = calculateGlobalTransform();
+            position = transform.row(3).toVector3d();
+            direction.y = Math.random() * 2 - 1;
+            physicsSpeed = speed;
+            serveTimer = 1;
+            return;
+        }
+
+        if (mouseDown) {
+            //Handle movement with mouse input
+            var perspectiveFactor = 1.4;
+            var halfScreenHeight = 240;
+            var target = (halfScreenHeight - getMousePosition().y) * perspectiveFactor;
+            if (Math.abs(position.y - target) > 6) {
+                if (position.y > target)
+                    moveDown();
+                else
+                    moveUp();
+            } else {
+                stopVertical();
+            }
+        } else {
+            stopVertical();
+        }
+
+        var next = Qt.vector3d(position.x, position.y, position.z);
+
+        direction = direction.normalized();
+
+        if (hasAI) {
+            //Handle AI movement input
+            var obstacleTransform = calculateGlobalTransform(obstacle1);
+            var obstaclePosition = obstacleTransform.row(3).toVector3d();
+            if (Math.abs(obstaclePosition.y - position.y) > 25)
+                input.y = Math.sign(obstaclePosition.y - position.y);
+            else
+                input.y = 0;
+        }
+
+        //Separate collision solving to X and Y axis
+        //Even if a collision happens on the X axis one can move along the Y axis and vice versa
+        var canMoveX = true;
+        var canMoveY = true;
+
+        if (hasPhysics) {
+            //Handle ball movement
+            next.x += direction.x * physicsSpeed * getDeltaTime();
+            next.y += direction.y * physicsSpeed * getDeltaTime();
+            if (next.y > 300 || next.y < -300)
+                canMoveY = false;
+        } else {
+            //Handle paddle movement
+            next.y += input.y * speed * getDeltaTime();
+        }
+
+        //Check collisions against two objects
+        var collisions = [collidesWith(obstacle1, position, next),
+                          collidesWith(obstacle2, position, next)];
+
+        var collisionPosition;
+        for (var i = 0; i < collisions.length; ++i) {
+            if (collisions[i].x || collisions[i].y)
+                collisionPosition = collisions[i].position;
+            if (collisions[i].x)
+                canMoveX = false;
+            if (collisions[i].y)
+                canMoveY = false;
+        }
+
+        if (canMoveX) {
+            setAttribute("position.x", next.x);
+        } else {
+            //Change ball direction and the angle if collision happens
+            if (hasPhysics) {
+                direction.x = -direction.x;
+                direction.y = -Math.sin(Math.atan2(collisionPosition.y - position.y,
+                                                   collisionPosition.x - position.x));
+                physicsSpeed += 20;
+            }
+        }
+
+        if (canMoveY) {
+            setAttribute("position.y", next.y);
+        }
+        else {
+            if (hasPhysics)
+                direction.y = -direction.y;
+        }
+    }
+
+    function collidesWith(obstacle, position, next) {
+        if (obstacle === "")
+            return {x: false, y: false};
+
+        //Get obstacle position
+        var obstacleTransform = calculateGlobalTransform(obstacle);
+        var obstaclePosition = obstacleTransform.row(3).toVector3d();
+
+        var ballSize = 50;
+        var paddleWidth = 50;
+        var paddleHeight = 100;
+
+        var xCollides = true;
+        var yCollides = true;
+
+        //Rectangular collision check
+        //All collisions are between the ball and a paddle so sizes can be hard-coded
+        if (next.x + paddleWidth / 2 < obstaclePosition.x - ballSize / 2
+            || next.x - paddleWidth / 2 > obstaclePosition.x + ballSize / 2
+            || position.y + paddleHeight / 2 < obstaclePosition.y - ballSize / 2
+            || position.y - paddleHeight / 2 > obstaclePosition.y + ballSize / 2) {
+            xCollides = false;
+        }
+
+        if (position.x + paddleWidth / 2 < obstaclePosition.x - ballSize / 2
+            || position.x - paddleWidth / 2 > obstaclePosition.x + ballSize / 2
+            || next.y + paddleHeight / 2 < obstaclePosition.y - ballSize / 2
+            || next.y - paddleHeight / 2 > obstaclePosition.y + ballSize / 2) {
+            yCollides = false;
+        }
+
+        return {position: obstaclePosition, x: xCollides, y: yCollides};
+    }
+
+    function onMouseDown() {
+        mouseDown = true;
+    }
+
+    function onMouseUp() {
+        mouseDown = false;
+    }
+
+    function moveUp() {
+        input.y = 1;
+    }
+
+    function moveDown() {
+        input.y = -1;
+    }
+
+    function moveLeft() {
+        input.x = -1;
+    }
+
+    function moveRight() {
+        input.x = 1;
+    }
+
+    function stopHorizontal() {
+        input.x = 0;
+    }
+
+    function stopVertical() {
+        input.y = 0;
+    }
+}
diff --git a/TennisGame/scripts/Score.qml b/TennisGame/scripts/Score.qml
new file mode 100644
index 0000000000000000000000000000000000000000..02308739e2cf3345602475d7d11aad17445d6a07
--- /dev/null
+++ b/TennisGame/scripts/Score.qml
@@ -0,0 +1,36 @@
+/*[[
+<Handler name="addPlayerScore" formalName="Add Player Score" />
+<Handler name="addComputerScore" formalName="Add Computer Score" />
+<Handler name="resetScores" formalName="Reset Scores" />
+
+<Event name="onPlayerWins" />
+<Event name="onComputerWins"/>
+]]*/
+
+import QtStudio3D.Behavior 1.0
+
+Behavior {
+    property var playerScore: 0
+    property var computerScore: 0
+
+    onUpdate: {
+        setAttribute("textstring", playerScore + " - " + computerScore);
+    }
+
+    function addPlayerScore() {
+        playerScore++;
+        if (playerScore >= 3)
+            fireEvent("onPlayerWins");
+    }
+
+    function addComputerScore() {
+        computerScore++;
+        if (computerScore >= 3)
+            fireEvent("onComputerWins");
+    }
+
+    function resetScores() {
+        playerScore = 0;
+        computerScore = 0;
+    }
+}