qml demo分析(maroon-小游戏)

时间:2023-03-09 13:04:26
qml demo分析(maroon-小游戏)

1、效果展示

  这篇文章我还是分析一个qt源码中的qml程序,程序运行效果如下图所示。

qml demo分析(maroon-小游戏)

图1  游戏开始

qml demo分析(maroon-小游戏)

图2  游戏中

2、源码分析

  这个游戏的源码文件比较多,为了能更清楚的了解整个代码,我先整体分析代码,然后再局部分析。

1、源码目录结构

qml demo分析(maroon-小游戏)

图3  源码目录

  如图3所示,是小游戏的源码目录,下边我分别按文件名称来介绍该文件的功能

  • TowerBase.qml:模型父类,定义了一些共有的属性,比如血量,攻击距离和攻击伤害等
  • Bomb.qml:海藻,父类为TowerBase.qml
  • Factory.qml:星星,父类为TowerBase.qml
  • Ranged.qml:章鱼,父类为TowerBase.qml
  • Melee.qml:螃蟹,父类为TowerBase.qml
  • BuildButton.qml:左键菜单中的一项
  • GameCanvas.qml:游戏中画布
  • GameOverScreen.qml:游戏结束画布
  • InfoBar.qml:游戏信息,包含当前血量显示,当前救下的鱼和金币数量
  • maroon.qml:程序主文件
  • MobBase.qml:带有鱼的气泡
  • NewGameScreen.qml:程序启动时画布,和GameCanvas、GameOverScreen组成了一张竖直的大画布
  • SoundEffect.qml:声音文件,可以播放音频文件
  • logic.js:js文件,完成一些具体的逻辑操作,比如新游戏清空内存,游戏推进更新内存等。

2、交互分析

  该游戏为一个塔防类游戏,类似于植物大战僵尸,但是游戏丰富程度和游戏流程度却要差上很多,不过既然是demo,我们就只学习它的方式方法。首先启动游戏,游戏界面使用NewGameScreen组件展示ui,启动页只包含游戏标题、带气泡的鱼和开始按钮,在动态图1的第一帧就可以看到,点击开始游戏,整个界面下滑,然后出现倒数3秒,并开始游戏,开始游戏画面用GameCanvas组件展示,但不包括游戏当前血量和金币数值等信息。随着游戏的推进,游戏的推进速度也会随之变快,这个时候如果自身血量小于等于0那么游戏就会结束,整个界面再次下滑,出现游戏结束后所得分数界面GameOverScreen,在这个界面我们还可以再一次启动程序,当点击新游戏时,整个界面上滑,又出现开始游戏画面GameCanvas。这个时候关于界面上滑和下滑就出现了一个循环,即GameCanvas和GameOverScreen相互转化。其实整个游戏画面是一个高度为当前游戏窗口高度不到3倍大的画布(游戏信息数据在游戏总和游戏结束均有)。

3、模型分析

  在这个小游戏中,总共有5个模型,分别是:海藻、星星、带鱼的泡泡、章鱼和螃蟹。除过带鱼的泡泡其余4个模型都有一个公有的基类TowerBase,因此这4个模型我就重点解释其中一个模型,其他模型的代码中也有大量的注释。

  Bomb是海藻组件,其中有一个关键对象SpriteSequence,他可以控制多个动画的渲染,在这个海藻组件中默认使用name为idle的Sprite,这是一个可以将png分段展示的对象。游戏中游戏推进时,fire接口会被调用,播放海藻爆炸前音频文件,当前动画改为shoot,并开启一个定时器,用于调用finishFire接口,完成击杀气泡拯救小鱼的操作,海藻组件代码如下,代码中亦有大量注释

 //海藻爆炸
import QtQuick 2.0
import "../logic.js" as Logic
import ".." TowerBase {
id: container
hp:
range: 0.4
rof:
property real detonationRange: 2.5 function fire() {//开始爆炸
sound.play()//首先播放声音
sprite.jumpTo("shoot")//
animDelay.start()//启动定时器
} function finishFire() {//爆炸结束
var sCol = Math.max(, col - )
var eCol = Math.min(Logic.gameState.cols - , col + )
var killList = new Array()
for (var i = sCol; i <= eCol; i++) {
for (var j = ; j < Logic.gameState.mobs[i].length; j++)
if (Math.abs(Logic.gameState.mobs[i][j].y - container.y) < Logic.gameState.squareSize * detonationRange)
killList.push(Logic.gameState.mobs[i][j])//满足爆炸距离的都加入到击杀列表
while (killList.length > )
Logic.killMob(i, killList.pop())//调用js函数击杀指定列所有目标
}
Logic.killTower(row, col);//移除海藻
} Timer {
id: animDelay
running: false
interval: shootState.frameCount * shootState.frameDuration//动画播放总时长
onTriggered: finishFire()
} function die()//销毁对象本身
{
destroy() // No blink, because we usually meant to die
} SoundEffect {//播放音频文件
id: sound
source: "../audio/bomb-action.wav"//海藻爆炸音频文件
} SpriteSequence {//动画序列
id: sprite
width:
height:
interpolate: false
goalSprite: "" Sprite {//海藻转向动画
name: "idle"
source: "../gfx/bomb-idle.png"
frameCount:
frameDuration: //每帧持续时长
} Sprite {//海藻爆炸动画
id: shootState
name: "shoot"
source: "../gfx/bomb-action.png"
frameCount:
frameDuration:
to: { "dying" : } //动画结束后 跳转到dying动画
} Sprite {//海藻爆炸动画
name: "dying"
source: "../gfx/bomb-action.png"//资源地址
frameCount: //只包含一帧
frameX: *
frameWidth:
frameHeight:
frameDuration:
} SequentialAnimation on x {//动画作用于x坐标
loops: Animation.Infinite
NumberAnimation { from: x; to: x + ; duration: ; easing.type: Easing.InOutQuad }
NumberAnimation { from: x + ; to: x; duration: ; easing.type: Easing.InOutQuad }
}
SequentialAnimation on y {//动画作用于y坐标
loops: Animation.Infinite
NumberAnimation { from: y; to: y - ; duration: ; easing.type: Easing.InOutQuad }
NumberAnimation { from: y - ; to: y; duration: ; easing.type: Easing.InOutQuad }
}
}
}

4、js文件分析

  js文件用于控制qml程序的逻辑实现部分,简单的js代码可以内联到qml组件代码里,如果是复杂的或者一些工具函数,那么最好还是写到一个单独的js文件,然后通过import导入到qml文件中。

  关于这个游戏的一些具体细节我个人没有仔细研究,比如游戏的速度控制。写此分析文章的原因主要是为了学习qml的语法和代码习惯,因此不尽如人意的地方可能会比较多,大神勿喷,仅供初学者借鉴。

  这个游戏的js文件主要是提供了一系列的工具函数,包括游戏进度控制的参数,具体接口含义可直接看如下代码中的注释

 .pragma library // 共享库  该文件只会被加载一次
.import QtQuick 2.0 as QQ // Game Stuff
var gameState // Local reference
function getGameState() { return gameState; } var towerData = [ // Name and cost, stats are in the delegate per instance
{ "name": "Melee", "cost": },
{ "name": "Ranged", "cost": },
{ "name": "Bomb", "cost": },
{ "name": "Factory", "cost": }
] var waveBaseData = [, , , , , , , , , , , , , ];
var waveData = []; var towerComponents = new Array(towerData.length);
var mobComponent = Qt.createComponent("mobs/MobBase.qml"); //游戏结束 释放动态申请的内存空间 并重置相应的标志
function endGame()
{
gameState.gameRunning = false;//游戏未在正在运行
gameState.gameOver = true;//游戏结束
for (var i = ; i < gameState.cols; i++) {
for (var j = ; j < gameState.rows; j++) {
if (gameState.towers[towerIdx(i, j)]) {
gameState.towers[towerIdx(i, j)].destroy();
gameState.towers[towerIdx(i, j)] = null;
}
}
for (var j in gameState.mobs[i])
gameState.mobs[i][j].destroy();
gameState.mobs[i].splice(,gameState.mobs[i].length); //Leaves queue reusable
}
} function startGame(gameCanvas)
{
waveData = new Array();
for (var i in waveBaseData)
waveData[i] = waveBaseData[i];
gameState.freshState();//重置游戏资料
for (var i = ; i < gameCanvas.cols; i++) {//清空游戏内存数据,主要针对每个格子存放数据
for (var j = ; j < gameCanvas.rows; j++)
gameState.towers[towerIdx(i, j)] = null;
gameState.mobs[i] = new Array();
}
gameState.towers[towerIdx(, )] = newTower(, , );//左上角生成一个星星
gameState.gameRunning = true;
gameState.gameOver = false;
} function newGameState(gameCanvas)//开始一场新游戏
{
for (var i = ; i < towerComponents.length; i++) {
towerComponents[i] = Qt.createComponent("towers/" + towerData[i].name + ".qml");
if (towerComponents[i].status == QQ.Component.Error) {
gameCanvas.errored = true;
gameCanvas.errorString += "Loading Tower " + towerData[i].name + "\n" + (towerComponents[i].errorString());
console.log(towerComponents[i].errorString());
}
}
gameState = gameCanvas;//gameState赋初值 这个时候才知道对象类型
gameState.freshState();//重置游戏数据
gameState.towers = new Array(gameCanvas.rows * gameCanvas.cols);//为游戏分配rows*cols个内存空间,用于存储每个格子数据
gameState.mobs = new Array(gameCanvas.cols);//
return gameState;
} function row(y)//返回所在行
{
return Math.floor(y / gameState.squareSize);
} function col(x)//返回所在列
{
return Math.floor(x / gameState.squareSize);
} function towerIdx(x, y)//根据行和列计算towers位置
{
return y + (x * gameState.rows);
} function newMob(col)//随机产生一个带鱼气泡
{
var ret = mobComponent.createObject(gameState.canvas,
{ "col" : col,
"speed" : (Math.min(2.0, 0.10 * (gameState.waveNumber + ))),
"y" : gameState.canvas.height });
gameState.mobs[col].push(ret);
return ret;
} function newTower(type, row, col)//根据类型生成模型,并设置模型所在行和列
{
var ret = towerComponents[type].createObject(gameState.canvas);
ret.row = row;
ret.col = col;
ret.fireCounter = ret.rof;
ret.spawn();
return ret;
} function buildTower(type, x, y)//根据菜单项类型、行和列 生成tower
{
if (gameState.towers[towerIdx(x,y)] != null) {//如果之前存在
if (type <= ) {//如果点击类型不在4个菜单项里 则清空该tower
gameState.towers[towerIdx(x,y)].sell();
gameState.towers[towerIdx(x,y)] = null;
}
} else {
if (gameState.coins < towerData[type - ].cost)//如果金额不够 直接退出
return;
gameState.towers[towerIdx(x, y)] = newTower(type - , y, x);//生成一个新的模型
gameState.coins -= towerData[type - ].cost;//减去建造模型 所需要的金币
}
} function killMob(col, mob)//移除指定列模型
{
if (!mob)
return;
var idx = gameState.mobs[col].indexOf(mob);
if (idx == - || !mob.hp)
return;
mob.hp = ;
mob.die();
gameState.mobs[col].splice(idx,);//从列中减掉
} function killTower(row, col)//销毁指定位置模型
{
var tower = gameState.towers[towerIdx(col, row)];
if (!tower)
return;
tower.hp = ;
tower.die();
gameState.towers[towerIdx(col, row)] = null;
} function tick()//游戏推进
{
if (!gameState.gameRunning)//游戏不在运行时 直接返回
return; // Spawn
gameState.waveProgress += ;//游戏推进
var i = gameState.waveProgress;
var j = ;
while (i > && j < waveData.length)
i -= waveData[j++];
if ( i == ) // Spawn a mob//生成一个气泡
newMob(Math.floor(Math.random() * gameState.cols));
if ( j == waveData.length ) { // Next Wave
gameState.waveNumber += ;//游戏等级+1
gameState.waveProgress = ;
var waveModifier = ; // Constant governing how much faster the next wave is to spawn (not fish speed)
for (var k in waveData ) // Slightly faster
if (waveData[k] > waveModifier)
waveData[k] -= waveModifier;
} // 遍历所有格子
for (var j in gameState.towers) {
var tower = gameState.towers[j];
if (tower == null)
continue;
if (tower.fireCounter > ) {
tower.fireCounter -= ;
continue;
}
var column = tower.col;//遍历所有气泡
for (var k in gameState.mobs[column]) {
var conflict = gameState.mobs[column][k];
if (conflict.y <= gameState.canvas.height && conflict.y + conflict.height > tower.y
&& conflict.y - ((tower.row + ) * gameState.squareSize) < gameState.squareSize * tower.range) { // 满足伤害距离
tower.fire();//
tower.fireCounter = tower.rof;
conflict.hit(tower.damage);//气泡自行处理伤害动作
}
} // 只有星星模型满足此条件 新增金币 并调用星星的fire动作
if (tower.income) {
gameState.coins += tower.income;
tower.fire();
tower.fireCounter = tower.rof;
}
} // 气泡移动
for (var i = ; i < gameState.cols; i++) {//遍历所有列
for (var j = ; j < gameState.mobs[i].length; j++) {//遍历每一列的气泡
var mob = gameState.mobs[i][j];//气泡
var newPos = gameState.mobs[i][j].y - gameState.mobs[i][j].speed;
if (newPos < ) {//如果浮出水面
gameState.lives -= ;//生命值减1
killMob(i, mob);//移除气泡
if (gameState.lives <= )//当生命值小于等于0时 游戏结束
endGame();//执行此操作之后 gameRunning状态变为false 则该tick函数会被调用 但不会往下执行
continue;
}
var conflict = gameState.towers[towerIdx(i, row(newPos))];//拿到指定位置模型
if (conflict != null) {
if (mob.y < conflict.y + gameState.squareSize)
gameState.mobs[i][j].y += gameState.mobs[i][j].speed * ; // 气泡上浮一下
if (mob.fireCounter > ) {
mob.fireCounter--;
} else {//气泡移动到守卫模型跟前
gameState.mobs[i][j].fire();//
conflict.hp -= mob.damage;//守卫模型受到伤害
if (conflict.hp <= )//当守卫模型血量小于等于0时 移除守卫模型
killTower(conflict.row, conflict.col);
mob.fireCounter = mob.rof;
}
} else {
gameState.mobs[i][j].y = newPos;//气泡移动到新位置
}
}
}
}

5、左键菜单项

 //游戏中左键菜单项
import QtQuick 2.0
import "logic.js" as Logic Item {
id: container
width:
height:
property alias source: img.source
property int index//当前点击列序
property int row: //当前菜单项所在行
property int col: //当前菜单项所在列
property int towerType//表明菜单项类型 根据此类型 可以获取name和cost值
property bool canBuild: true//菜单项是否可以被创建
property Item gameCanvas: parent.parent.parent
signal clicked()//自定义信号 当该控件被点击时 Image {
id: img
opacity: (canBuild && gameCanvas.coins >= Logic.towerData[towerType-].cost) ? 1.0 : 0.4//当金币数不够时,该菜单项透明度变为40%
}
Text {//菜单项右上角数字
anchors.right: parent.right
font.pointSize:
font.bold: true
color: "#ffffff"
text: Logic.towerData[towerType - ].cost
}
MouseArea {//鼠标点击时 根据菜单项类型、行数和列数新建模型
anchors.fill: parent
onClicked: {
Logic.buildTower(towerType, col, row)//调用js方法 生成一个新的tower
container.clicked()//发出菜单项被点击信号
}
}
Image {//下三角
visible: col == index && row != //当列号等于当前点击列时 并且不是第一行
source: "gfx/dialog-pointer.png"
anchors.top: parent.bottom
anchors.topMargin:
anchors.horizontalCenter: parent.horizontalCenter
}
Image {//上倒三角
visible: col == index && row == //当列号等于当前点击列时 并且是第一行
source: "gfx/dialog-pointer.png"
rotation:
anchors.bottom: parent.top//三角的底部紧接父控件顶部
anchors.bottomMargin:
anchors.horizontalCenter: parent.horizontalCenter
}
}

6、主qml,用于整个程序ui布局

 //程序主文件,由此qml启动整个ui
import QtQuick 2.0
import QtQuick.Particles 2.0
import "content"
import "content/logic.js" as Logic Item {
id: root
width:
height:
property var gameState//游戏状态 就是游戏场景GameCanvas 维护了大量游戏过程的信息
property bool passedSplash: false//游戏开始标志 只有首次启动游戏时为false 后续游戏结束重新开始依赖于gameOver状态 Image {
source:"content/gfx/background.png" //背景图高度为主窗口3倍大小 1:失败重新开始 2:游戏中 3:启动游戏
anchors.bottom: view.bottom//背景图和窗口底部对齐 ParticleSystem {//粒子系统
id: particles
anchors.fill: parent ImageParticle {//图片粒子 表示气泡
id: bubble
anchors.fill: parent
source: "content/gfx/catch.png"
opacity: 0.25//透明度25%
} Wander {
xVariance: ;
pace: ;
} Emitter {//粒子发射器 规定发射粒子规则
width: parent.width
height:
anchors.bottom: parent.bottom
anchors.bottomMargin:
startTime: //每隔15s发送一次粒子 emitRate: //每次发送粒子数目 默认每秒钟发送10个
lifeSpan: //最多存活15s acceleration: PointDirection{ y: -; xVariation: ; yVariation: }//加速度 y值减小 x方向和y方向存在2个偏移 size: //初始大小
sizeVariation: //大小可上下浮动范围
}
}
} Column {
id: view
y: -(height - )
width: //游戏结束 对应背景图中序号1
GameOverScreen { gameCanvas: canvas }//绑定GameCanvas到gameCanvas导出对象上,主要为了获取游戏结束时,救了多少条鱼和修改游戏状态 //游戏中
Item {
id: canvasArea
width:
height: Row {//游戏中 顶部波浪1
height: childrenRect.height
Image {
id: wave
y: //距离顶部30像素
source:"content/gfx/wave.png"//960*70
}
Image {
y: //距离顶部30像素
source:"content/gfx/wave.png"
}
NumberAnimation on x { from: ; to: -(wave.width); duration: ; loops: Animation.Infinite }//向左走
SequentialAnimation on y {//平方向移动的过程中,垂直方向进行序列动画
loops: Animation.Infinite
NumberAnimation { from: y - ; to: y + ; duration: ; easing.type: Easing.InOutQuad }
NumberAnimation { from: y + ; to: y - ; duration: ; easing.type: Easing.InOutQuad }
}
} Row {//游戏中 顶部波浪2
opacity: 0.5
Image {
id: wave2
y:
source: "content/gfx/wave.png"
}
Image {
y:
source: "content/gfx/wave.png"
}
NumberAnimation on x { from: -(wave2.width); to: ; duration: ; loops: Animation.Infinite }//向右走
SequentialAnimation on y {
loops: Animation.Infinite
NumberAnimation { from: y + ; to: y - ; duration: ; easing.type: Easing.InOutQuad }
NumberAnimation { from: y - ; to: y + ; duration: ; easing.type: Easing.InOutQuad }
}
} Image {//阳光照射效果1
source: "content/gfx/sunlight.png"
opacity: 0.02
y:
anchors.horizontalCenter: parent.horizontalCenter
transformOrigin: Item.Top//偏移起始位置
SequentialAnimation on rotation {//在图片旋转属性上使用序列动画 即:动画依次执行
loops: Animation.Infinite//动画无限循环
NumberAnimation { from: -; to: ; duration: ; easing.type: Easing.InOutSine }
NumberAnimation { from: ; to: -; duration: ; easing.type: Easing.InOutSine }
}
} Image {//阳光照射效果2
source: "content/gfx/sunlight.png"
opacity: 0.04
y:
anchors.horizontalCenter: parent.horizontalCenter
transformOrigin: Item.Top
SequentialAnimation on rotation {
loops: Animation.Infinite
NumberAnimation { from: ; to: -; duration: ; easing.type: Easing.InOutSine }
NumberAnimation { from: -; to: ; duration: ; easing.type: Easing.InOutSine }
}
} Image {//背景网格
source: "content/gfx/grid.png"
opacity: 0.5
}
//游戏场景
GameCanvas {
id: canvas
anchors.bottom: parent.bottom
anchors.bottomMargin:
x:
focus: true
} InfoBar { anchors.bottom: canvas.top; anchors.bottomMargin: ; width: parent.width } //3..2..1..go
Timer {
id: countdownTimer
interval:
running: root.countdown <
repeat: true
onTriggered: root.countdown++//
}
Repeater {//倒数数据
model: ["content/gfx/text-blank.png", "content/gfx/text-3.png", "content/gfx/text-2.png", "content/gfx/text-1.png", "content/gfx/text-go.png"]
delegate: Image {
visible: root.countdown <= index
opacity: root.countdown == index ? 0.5 : 0.1
scale: root.countdown >= index ? 1.0 : 0.0
source: modelData
Behavior on opacity { NumberAnimation {} }
Behavior on scale { NumberAnimation {} }//NumberAnimation继承自PropertyAnimation,duration值默认为250ms
}
}
}
//游戏启动页 仅仅包含游戏标题、带气泡的鱼和开始按钮 背景色和上浮的气泡是由根元素下的粒子系统(particles)完成
NewGameScreen {
onStartButtonClicked: root.passedSplash = true//游戏开始
}
} property int countdown:
Timer {
id: gameStarter
interval:
running: false
repeat: false
onTriggered: Logic.startGame(canvas);//等待4s中启动新的一局 等待的过程中countdownTimer定时器 每隔1s更新倒数图片 显示3 2 1 go
} states: [
State {//游戏开始 当在游戏第一次启动时gameOver为false passedSplash为false
name: "gameOn";
when: gameState.gameOver == false && passedSplash //当游戏状态不为结束并且passedSplash为真时触发 passedSplash值由NewGameScreen信号的处理槽函数修改
PropertyChanges { target: view; y: -(height - ) }//480标示显示第二页
StateChangeScript { script: root.countdown = ; }//countdown重置为0 开始新游戏时 倒数3 2 1
PropertyChanges { target: gameStarter; running: true }//调用游戏开始定时器 启动游戏
},
State {//对应背景图第1页
name: "gameOver";
when: gameState.gameOver == true //当游戏状态为结束时触发
PropertyChanges { target: view; y: }//Column定位器滚动到最顶端 也即游戏结束,请重新开始页面
}
] transitions: Transition {//对指定属性进行动画过渡
NumberAnimation { properties: "x,y"; duration: ; easing.type: Easing.OutQuad }
}
//组件加载完毕时,背景图定位到第三页,即游戏启动页
Component.onCompleted: gameState = Logic.newGameState(canvas);
}

3、源码下载

  qml maroon小游戏