main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQuick/QQuickView>
#include <QtQml/QQmlEngine>
#include <QtGui/QOpenGLContext>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
//format用于渲染的 OpenGL 配置文件和版本、是否启用立体缓冲区以及交换行为。
QSurfaceFormat format;
if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL)
{
format.setVersion(3, 2);
//设置所需的 OpenGL 上下文配置文件
format.setProfile(QSurfaceFormat::CoreProfile);
}
//将最小深度缓冲区大小设置为 size。
format.setDepthBufferSize(24);
//将首选模板缓冲区大小设置为大小位
format.setStencilBufferSize(8);
QQuickView view;
view.setFormat(format);
view.setResizeMode(QQuickView::SizeRootObjectToView);
QObject::connect(view.engine(), &QQmlEngine::quit, &app, &QGuiApplication::quit);
view.setSource(QUrl("qrc:/main.qml"));
view.show();
return app.exec();
}
main.qml
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Scene3D 2.0
Item
{
id: mainView
property int score: 0
readonly property alias window: mainView
width: 1280; height: 768
visible: true
Keys.onEscapePressed:
{
Qt.quit()
}
//
Rectangle
{
id: hud
color: "#31363b"
anchors.left: parent.left
anchors.right: parent.right
anchors.top : parent.top
height: 60
//上面的分数
Label
{
id: snakeSizeText
anchors.centerIn: parent
font.pointSize: 25
color: "white"
text: "Score: " + score
}
}
//游戏场景
Scene3D
{
id:scene
anchors.top: hud.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
focus: true
aspects: "input"
GameArea
{
id:gameArea
initialSnakeSize: 5
}
}
//状态
OverlayItem
{
id: overlayItem
anchors.fill: mainView
visible: false
//qml signal/slot connection
Connections
{
target: gameArea
onStateChanged:
{ //This item contains the state variable, so we can be notified when the state variable is updated using onStateChanged
overlayItem.state = gameArea.state;
}
}
}
}
OverlayItem.qml
import QtQuick 2.0
Item
{
id: root
states:
[
//两种状态
State
{
name: "PLAY"
PropertyChanges { target: root; visible: false }
},
State
{
name: "GAMEOVER"
//PropertyChanges 用于定义状态中的属性值或绑定。 这使项目的属性值可以在状态之间更改时更改。
PropertyChanges { target: root; visible: true }
PropertyChanges { target: gameOver; visible: true }
}
]
GameOverItem
{
id: gameOver
}
}
GameArea.qml
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Extras 2.0
import Qt3D.Input 2.0
import QtQuick 2.9 as QQ2 //防止名字冲突
import "engine.js" as Engine
Entity
{
id: root
property alias gameRoot: root
property alias timerInterval: timer.interval
property int initialTimeInterval: 500
property int initialSnakeSize: 5
property string state: ""
//创建摄像头
Camera
{
id: camera
property real x: 24.5
property real y: 14.0
projectionType: CameraLens.PerspectiveProjection
fieldOfView: 45
aspectRatio: 16/9
nearPlane : 0.1
farPlane : 1000.0
通过将相机放置在 Qt.vector3d(x, y, 33)处,位于屏幕外
position: Qt.vector3d( x, y, 33.0 )
upVector: Qt.vector3d( 0.0, 1.0, 0.0 )
viewCenter: Qt.vector3d( x, y, 0.0 )
}
Entity
{
id: sun
components:
[
//定向光是一种行为类似于太阳光的光源。 来自定向光的光从同一方向以相同的强度照射所有对象,无论它们在场景中的哪个位置。
DirectionalLight
{
color: Qt.rgba(0.8, 0.8, 0.8, 0.5)
worldDirection: Qt.vector3d(-0.6, -0.5, -1)
}
]
}
//渲染设置
//RenderSettings 类型保存与渲染过程相关的设置并托管FrameGraph
RenderSettings
{
id: frameFraph
activeFrameGraph: ForwardRenderer
{
//黑色背景
clearColor: Qt.rgba(0, 0, 0, 1)
camera: camera
}
}
//捕获键盘事件; 它负责将按键事件分派给活动的 KeyboardHandler
KeyboardDevice
{
id:keyboardController
}
InputSettings { id: inputSettings }
QQ2.Component.onCompleted:
{
console.log("Start game...");
Engine.start();
timer.start()
}
QQ2.Timer
{
id:timer
interval: initialTimeInterval
repeat: true
onTriggered: Engine.update()
}
KeyboardHandler
{
//KeyboardHandler 组件附加到控制器,每次按下一个键时都会调用 onPressed() 函数
id: input
sourceDevice: keyboardController
focus: true
onPressed: Engine.handleKeyEvent(event);
}
Background
{
position: Qt.vector3d(camera.x,camera.y,0)
scale3D: Qt.vector3d(camera.x * 2,camera.y * 2,0)
}
components: [frameFraph, input]
}
Wall:这代表了蛇不能去的区域
SnakePart:这代表蛇身体的一部分
Apple:这代表在随机位置生成的苹果
Background:这个代表了蛇和苹果后面的背景
import Qt3D.Core 2.0
Entity
{
property int type: 0
property vector2d gridPosition: Qt.vector2d(0, 0)
}
wall
import Qt3D.Core 2.0
import "factory.js" as Factory
//墙壁没有任何视觉表现,它是不可见的
GameEntity
{
id: root
type: Factory.WALL_TYPE
property alias position: transform.translation
Transform
{
id: transform
}
components: [transform]
}
SnakePart.qml
如果添加到 GameArea 场景中,SnakePart 块将显示一个绿色立方体。 SnakePart 块不是完整的蛇,而是它身体的一部分。 请记住,蛇每吃一个苹果就会长大。 意味着将 SnakePart 的新实例添加到 SnakePart 中。
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Extras 2.0
import "factory.js" as Factory
GameEntity
{
id: root
type: Factory.SNAKE_TYPE
property alias position: transform.translation
phong 照明效果基于 3 个照明组件环境、漫反射和镜面反射的组合。 这些组件的相对强度通过它们的反射系数来控制,这些系数被建模为 RGB 三元组:
环境光是物体在没有任何其他光源的情况下发出的颜色。
漫反射是为粗糙表面反射而发射的颜色。
镜面反射是光亮表面反射所发出的颜色。
表面的光泽度由浮动属性控制。
此材质使用具有单一渲染通道方法的效果并执行每个片段照明。
PhongMaterial
{
id: material
diffuse: "green"
}
//一个长方体网格
CuboidMesh
{
id: mesh
}
Transform
{
id: transform
}
components: [material, mesh, transform]
}
Background.qml
import Qt3D.Core 2.0
import Qt3D.Render 2.0
import Qt3D.Extras 2.0
Entity
{
id: root
property alias position: transform.translation
property alias scale3D: transform.scale3D
MaterialBackground
{
id: material
}
CuboidMesh
{
id: mesh
}
Transform
{
id: transform
}
components: [material, mesh, transform]
}
背景块将显示在蛇和苹果的后面。 乍一看,这个实体看起来很像 SnakePart。 但是,Material 不是 Qt3D 类。 它是一种依赖于着色器的自定义材质。 让我们看看 MaterialBackground.qml:
MaterialBackground.qml
import Qt3D.Core 2.0
import Qt3D.Render 2.0
Material
{
id: material
//保存材质使用的参数列表
parameters:
[
Parameter
{
name: "score"
value: score
},
Parameter
{
name: "resolution"
value: Qt.vector2d(window.width, window.height)
},
Parameter
{
name: "diffuseTexture"
value: Texture2D
{
id: diffuseTexture
minificationFilter: Texture.LinearMipMapLinear
magnificationFilter: Texture.Linear
wrapMode
{
x: WrapMode.Repeat
y: WrapMode.Repeat
}
generateMipMaps: true
maximumAnisotropy: 16.0
TextureImage
{
source: "qrc:/images/grass.jpg"
}
}
}
]
//Qt3D 以一种非常方便的方式支持着色器。 只需将您的着色器文件添加到 .qrc 资源文件并将其加载到给定材质的效果属性中。
effect: Effect
{
FilterKey
{
id:forward
name: "renderingStyle"
value: "forward"
}
techniques:
[
Technique
{
filterKeys: [forward]
graphicsApiFilter
{
api: GraphicsApiFilter.OpenGL
profile: GraphicsApiFilter.CoreProfile //fix
majorVersion: 3
minorVersion: 2
}
renderPasses: RenderPass
{
shaderProgram: ShaderProgram
{
vertexShaderCode: loadSource("qrc:/shaders/gl3/grass.vert")
fragmentShaderCode: loadSource("qrc:/shaders/gl3/grass.frag")
}
}
},
Technique
{
filterKeys: [forward]
graphicsApiFilter
{
api: GraphicsApiFilter.OpenGLES
profile: GraphicsApiFilter.CoreProfile //fix
majorVersion: 2
minorVersion: 0
}
renderPasses: RenderPass
{
shaderProgram: ShaderProgram
{
vertexShaderCode: loadSource("qrc:/shaders/es2/grass.vert")
fragmentShaderCode: loadSource("qrc:/shaders/es2/grass.frag")
}
}
}
]
}
}
像国际象棋一样,这个棋盘将由行和列组成。 但是在我们的蛇游戏中,每个方格可以是:
苹果
一条蛇
一堵墙
空的
以下board的示例
这是一个 10x8 的board; 在游戏区周围有墙(W)。 一个苹果 (A) 以 7x2 生成。 最后,我们有一条蛇(S)从 3x4 开始到 5x5 结束
board.js
function Board(columnCount, rowCount, blockSize)
{
//行列数
this.columnCount = columnCount;
this.rowCount = rowCount;
//方块大小
this.blockSize = blockSize;
this.maxIndex = columnCount * rowCount;
this.data = new Array(this.maxIndex);
}
Board.prototype.init = function()
{
for (var i = 0; i < this.data.length; i++)
{
this.data[i] = null;
}
}
Board.prototype.index = function(column,row)
{
return column + (row * this.columnCount);
}
Board.prototype.setData = function(data, column, row)
{
this.data[this.index(column, row)] = data;
}
Board.prototype.at = function(column,row)
{
return this.data[this.index(column,row)];
}
factory.js
var SNAKE_TYPE = 1;
var WALL_TYPE = 2;
var APPLE_TYPE = 3;
//首先,我们定义所有游戏实体类型。 在我们的例子中,我们有苹果、蛇和墙类型。 然后,我们从 QML 文件创建游戏项目组件。 工厂将使用这些组件来动态创建新的游戏实体。
var snakeComponent = Qt.createComponent("SnakePart.qml");
var wallComponent = Qt.createComponent("Wall.qml");
var appleComponent = Qt.createComponent("Apple.qml");
//我们现在可以添加构造函数和 removeAllEntities() 实用程序函数来删除所有实例化的实体:
function GameFactory()
{
//这个工厂有三个成员变量:
//描述的游戏board的引用
this.board = null;
//parentEntity 变量的引用即游戏区域
this.parentEntity = null;
//一个实体数组,保留对创建的项目的引用
this.entities = [];
}
//调用者提供了实体类型和board坐标(列和行)
GameFactory.prototype.createGameEntity = function(type,column,row)
{
var component;
//一旦我们有了组件,我们就可以调用 component.createObject() 来创建这个组件的实例
switch(type)
{
case SNAKE_TYPE:
component = snakeComponent;
break;
case WALL_TYPE:
component = wallComponent;
break;
case APPLE_TYPE:
component = appleComponent;
break;
}
//这个新组件的父级将是 this.parentEntity,在我们的例子中是 GameArea
var gameEntity = component.createObject();
gameEntity.setParent(this.parentEntity);
// console.log("");
//然后,我们可以更新板,更新实体位置,并将这个新实体添加到实体数组中
this.board.setData(gameEntity, column, row);
gameEntity.gridPosition = Qt.vector2d(column, row);
gameEntity.position.x = column * this.board.blockSize;
gameEntity.position.y = row * this.board.blockSize;
this.entities.push(gameEntity);
return gameEntity;
}
//我们现在可以添加构造函数和 removeAllEntities() 实用程序函数来删除所有实例化的实体:
GameFactory.prototype.removeAllEntities = function()
{
//removeAllEntities() 函数将从它们的父项(即游戏区域)中删除项目并创建一个新的空实体数组。 这确保旧实体被垃圾收集器删除
for(var i = 0; i < this.entities.length; i++)
{
this.entities[i].setParent(null);
}
}
engine.js
//第一行是从另一个 JavaScript 文件导入 JavaScript 文件的 Qt 方法
.import "factory.js" as Factory
.import "board.js" as Board
//然后,我们可以轻松实例化工厂变量和 50x29 board的变量
var COLUMN_COUNT = 50;
var ROW_COUNT = 29;
var BLOCK_SIZE = 1;
var factory = new Factory.GameFactory();
var board = new Board.Board(COLUMN_COUNT,ROW_COUNT,BLOCK_SIZE);
var snake = [];
var direction;
function start()
{
//提供了启动引擎时所做的工作
//初始化引擎
initEngine();
//创建蛇
createSnake();
//在游戏区域周围创建墙壁
createWalls();
//生成第一个苹果
spawnApple();
//将 GameArea 状态切换为 PLAY
gameRoot.state = "PLAY";
}
//初始化引擎
function initEngine()
{
//此函数初始化并重置所有变量
//第一个任务是将 GameArea 计时器间隔设置为其初始值
//蛇每吃一个苹果,这个间隔就会缩短
//从而提高蛇的移动速度
timer.interval = initialTimeInterval;
score = 0;
//然后我们初始化工厂,提供对 board 和 gameRoot 引用
factory.board = board;
//gameRoot 指的是 GameArea
//该实体将是工厂实例化的所有项目的父级
factory.parentEntity = gameRoot;
factory.removeAllEntities();
//我们从工厂中移除所有现有实体并调用板的 init() 函数来清除板
board.init();
//最后,我们为蛇设置了默认方向
//向量 -1,0 表示蛇将开始向左移动
direction = Qt.vector2d(-1, 0);
}
//创建初始蛇
function createSnake()
{
snake = [];
var initialPosition = Qt.vector2d(25,12);
for(var i = 0; i < initialSnakeSize;i++)
{
snake.push(factory.createGameEntity(Factory.SNAKE_TYPE,
initialPosition.x + i,
initialPosition.y));
}
}
//在游戏区域周围创建墙壁
function createWalls()
{
for(var x = 0;x < board.columnCount;x++)
{
factory.createGameEntity(Factory.WALL_TYPE, x, 0);
factory.createGameEntity(Factory.WALL_TYPE, x, board.rowCount - 1);
}
for(var y = 1; y<board.rowCount - 1;y++)
{
factory.createGameEntity(Factory.WALL_TYPE,0,y);
factory.createGameEntity(Factory.WALL_TYPE, board.columnCount - 1, y);
}
}
//生成第一个苹果
function spawnApple()
{
var isFound = false;
var position;
while(!isFound)
{
position = Qt.vector2d(Math.floor(Math.random() * board.columnCount),
Math.floor(Math.random() * board.rowCount));
if((board.at(position.x,position.y) === null))
{
isFound = true;
}
}
factory.createGameEntity(Factory.APPLE_TYPE, position.x, position.y);
if(timerInterval > 10)
{
timerInterval -= 2;
}
}
function moveSnake(column,row)
{
var last = snake.pop();
board.setData(null,last.gridPosition.x, last.gridPosition.y);
setPosition(last,column,row);
snake.unshift(last);
}
function update()
{
if(gameRoot.state === "GAMEOVER")
{
return;
}
var headPosition = snake[0].gridPosition;
var newPosition = headPosition.plus(direction);
var itemOnNewPosition = board.at(newPosition.x,newPosition.y);
if(itemOnNewPosition === null)
{
moveSnake(newPosition.x,newPosition.y);
return;
}
switch(itemOnNewPosition.type)
{
case Factory.SNAKE_TYPE:
gameRoot.state = "GAMEOVER";
break;
case Factory.WALL_TYPE:
gameRoot.state = "GAMEOVER";
break;
case Factory.APPLE_TYPE:
itemOnNewPosition.setParent(null);
board.setData(null,newPosition.x,newPosition.y);
snake.unshift(factory.createGameEntity(
Factory.SNAKE_TYPE,
newPosition.x,
newPosition.y));
spawnApple();
score++;
break;
}
}
function setPosition(item, column,row)
{
board.setData(item,column,row);
item.gridPosition = Qt.vector2d(column,row);
item.position.x = column * board.blockSize;
item.position.y = row * board.blockSize;
}
function handleKeyEvent(event)
{
switch(event.key)
{
case Qt.Key_R:
start();
break;
case Qt.Key_W:
if(direction !== Qt.vector2d(0,-1))
{
direction = Qt.vector2d(0,1);
}
break;
case Qt.Key_A:
if(direction !== Qt.vector2d(-1,0))
{
direction = Qt.vector2d(1,0);
}
break;
case Qt.Key_S:
if(direction !== Qt.vector2d(0,1))
{
direction = Qt.vector2d(0,-1);
}
break;
case Qt.Key_D:
if(direction !== Qt.vector2d(1,0))
{
direction = Qt.vector2d(-1,0);
}
break;
}
}
function debugLogBoard()
{
console.log("");
for (var row = board.rowCount -1 ; row >= 0; row--)
{
var line = "";
for (var column = 0; column < board.columnCount; column++)
{
var item = board.at(column, row);
if (item === null)
{
line += "."
} else
{
line += item.type;
}
}
console.log(line);
}
}
我们现在将创建一个“游戏结束”HUD,在您输掉游戏时显示。 创建一个新文件 GameOverItem.qml:
GameOverItem.qml
import QtQuick 2.0
import QtQuick.Controls 2.4
Item
{
id: root
anchors.fill: parent
onVisibleChanged:
{
scoreLabel.text = "Your score: " + score
}
//一个黑色矩形填充整个屏幕
Rectangle
{
anchors.fill: parent
color: "black"
//不透明度值为 75%
//因此,游戏区域仍将在游戏结束屏幕后面 25% 处可见
opacity: 0.75
}
//显示文本“游戏结束”的 gameOverLabel 标签
Label
{
id: gameOverLabel
anchors.centerIn: parent
color: "white"
font.pointSize: 50
text: "Game Over"
}
Label
{
id: scoreLabel
width: parent.width
anchors.top: gameOverLabel.bottom
horizontalAlignment: "AlignHCenter"
color: "white"
font.pointSize: 20
}
Label
{
width: parent.width
anchors.bottom: parent.bottom
anchors.bottomMargin: 50
horizontalAlignment: "AlignHCenter"
color: "white"
font.pointSize: 30
text:"Press R to restart the game"
}
}
OverlayItem.qml
import QtQuick 2.0
//您可以看到 states 元素是一个 Item 属性
Item
{
id: root
//默认情况下, states 元素包含一个空字符串 state。 这里我们定义了两个名为 PLAY 和 GAMEOVER 的状态项。
states:
[
//之后我们可以将属性值绑定到一个状态
State
{
name: "PLAY"
PropertyChanges { target: root; visible: false }
},
State
{
name: "GAMEOVER"
//在我们的例子中,当状态为 GAMEOVER 时,我们将此 OverlayItem 及其 GameOverItem 的可见性设置为 true
//否则,对于状态 PLAY,我们将其隐藏
PropertyChanges { target: root; visible: true }
PropertyChanges { target: gameOver; visible: true }
}
]
GameOverItem
{
id: gameOver
}
}