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; + } +}