用Html5结合Qt制作一款本地化EXE游戏-太空大战(Space War)

本次来说一说如何利用lufylegend.js引擎制作一款html5游戏后将其通过Qt转换成EXE程序。步骤其实非常简单,接下来就一步步地做一下解释和说明。

首先我们来开发一个有点类似于太空大战的游戏,游戏截图如下:


游戏介绍:这个游戏原本是七十一雾央前辈用Cocos2d-x开发的android小游戏。由于我看到这个游戏实现起来比较简单,因此就把apk下载下来,并且在雾央的指导下,把它当rar压缩文件解开了,把素材偷走了……嘿嘿。由于我最近的开发涉及html5领域,因此就用html5+lufylegend.js把这个游戏移植到浏览器平台上了。当然,效率不能和雾央的原版游戏比,因为html5的效率众所周知是很低的。

操作说明:用鼠标点击界面,发出子弹攻击迎面飞来的敌人。

游戏目标:不放过任何一个迎面飞来的敌人。

游戏测试地址:

http://www.cnblogs.com/yorhom/articles/3274940.html

注:演示地址中没有背景音乐,是因为我觉得音乐太占空间了,所以去掉了。下载包里含有音乐,各位可以欣赏一下。另外也感谢一下雾央兄弟,感谢他给我提供那么好,那么多的素材。


接下来就来说说这个游戏的制作步骤。


准备工作

首先你需要下载lufylegend.js游戏引擎。这个引擎是一个html5开源库件,利用他可以仿照了As 3.0的语法进行html5开发,使用起来非常方便。当然,你说你不是flasher,不懂As 3.0,那也无妨,可以参照官方API文档进行学习。具体的介绍还是去官方网站看看吧,免得lufy说我乱介绍他的引擎,嘿嘿~

引擎官方网站:

http://lufylegend.com/lufylegend

引擎API文档:

http://lufylegend.com/lufylegend/api

因为本次开发要用到这个引擎,所以各位先看看这个引擎的一些API介绍吧,避免文中用到的一些API大伙看不懂。


制作过程

首先要读取一下游戏中的数据。本次开发要用到的数据如下:

/**加载变量*/
var loadData = [
	{path:"./js/Bullet.js",type:"js"},
	{path:"./js/Plain.js",type:"js"},
	{path:"./js/Background.js",type:"js"},
	{name:"bullet",path:"./images/bullet.png"},
	{name:"sky.1",path:"./images/gamebg0.png"},
	{name:"sky.2",path:"./images/gamebg1.png"},
	{name:"over_text",path:"./images/gameover.png"},
	{name:"player",path:"./images/hero.png"},
	{name:"monster",path:"./images/monster.png"},
	{name:"over_bg",path:"./images/overbg.png"},
	{name:"start_bg",path:"./images/startbg.png"},
	{name:"start_button_normal",path:"./images/startNormal.png"},
	{name:"start_button_selected",path:"./images/startSelected.png"}
];
由于加载完成后要保存这些加载好的数据,所以还要用一个变量:

var datalist;
接下来把一些定义的变量放在下面,都写了注释,大家慢慢看喔~

/**层变量*/
var backLayer,
plainLayer,
enemyLayer,
bulletLayer,
textLayer,
loadingLayer;

/**分数变量*/
var score;

/**频率变量*/
var maxFrame = 30;
var frameIndex = 0;

/**游戏进行时间*/
var gameTime;

/**对象变量*/
//显示分数对象
var scoreText;
//玩家
var player;
//音乐对象
var startMusic,
overMusic,
playingMusic,
dieMusic;

然后用到init初始化游戏,因为游戏是要跨平台的,所以要在手机上全屏显示,为了实现这些,我们在Main.js顶部加入如下的代码:

//设置全屏
LSystem.screen(LStage.FULL_SCREEN);
//初始化游戏
init(30,"mylegend",800,480,main);
init的用法和LSystem.screen的用法都可以参照API文档。

接下来我们来看看main函数,这个函数是用来加载图片和设置一些信息用的,比如开启debug模式等,代码如下:

function main(){
	//设置debug模式
	LStage.setDebug(true);
	//如果是移动端,就将body标签margin调为0px 0px 0px 0px
	if(LStage.canTouch == true){
		document.body.style.margin = "0px 0px 0px 0px";
	}
	
	//初始化加载层
	loadingLayer = new LoadingSample3();
	addChild(loadingLayer);
	
	//加载游戏数据
	LLoadManage.load(
		loadData,
		function(progress){
			//显示加载进度
			loadingLayer.setProgress(progress);
		},
		gameInit
	);
}
上面的代码中,用了库件中的LLoadManage类读取,这个类可以读取js文件和图片、音频文件等,大家可以自己去看看API文档。

接下来看看gameInit里的代码,这个函数是用来保存加载数据,加入音乐,加入开始场景用的。代码如下:

function gameInit(result){
	//保存加载的数据
	datalist = result;
	//清空加载层
	removeChild(loadingLayer);
	
	//加入地板层
	backLayer = new LSprite();
	addChild(backLayer);
	
	//初始化音乐
	initMusic();
	
	//加入开始界面
	addStartPage();
}
其中调用到的函数代码分别如下:

function initMusic(){
	//开场音乐
	startMusic = new LSound("./music/startbg.mp3");
	//结束音乐
	overMusic = new LSound("./music/overbg.mp3");
	//游戏开始后的音乐
	playingMusic = new LSound("./music/gamebg.wav");
	//死亡后的音乐
	dieMusic = new LSound("./music/die.wav");
}
function addStartPage(){
	//播放音乐
	startMusic.play(0,100000000000000000000000000000000);
	//加入背景
	var bitmapData = new LBitmapData(datalist["start_bg"]);
	var bitmap = new LBitmap(bitmapData);
	backLayer.addChild(bitmap);
	
	//按钮普通时的样式
	var normalBtnStyleData = new LBitmapData(datalist["start_button_normal"]);
	var normalBtnStyle = new LBitmap(normalBtnStyleData);
	//按钮盘旋时的样式
	var selectedBtnStyleData = new LBitmapData(datalist["start_button_selected"]);
	var selectedBtnStyle = new LBitmap(selectedBtnStyleData);
	//加入开始按钮
	var startBtn = new LButton(normalBtnStyle,selectedBtnStyle);
	startBtn.x = (LStage.width-startBtn.getWidth())*0.5;
	startBtn.y = (LStage.height-startBtn.getHeight())*0.5;
	backLayer.addChild(startBtn);
	
	//加入开始事件
	startBtn.addEventListener(LMouseEvent.MOUSE_DOWN,startGame);
}
代码都加上了注释,可以参照API文档看看。在上面的addStartPage代码中,加入一个开始按钮后,我们给这个按钮加了一个鼠标事件,这个事件是用来触发游戏开始用的。开始游戏我们用的是startGame函数,代码如下:

function startGame(event){
	//清空界面
	backLayer.removeAllChild();
	
	//分数调零
	score = 0;
	//游戏时间调零
	gameTime = 0;
	
	//停止开始界面的音乐音乐
	startMusic.close();
	//播放游戏进行中的音乐
	playingMusic.play(0,100000000000000000000000000000000);
	
	//加入滚动背景
	var background = new Background();
	backLayer.addChild(background);
	
	//初始化层变量
	initLayer();
	//加入玩家飞机
	player = new LBitmap(new LBitmapData(datalist["player"]));
	player.x = 20;
	player.y = (LStage.height-player.getHeight())*0.5
	plainLayer.addChild(player);
	
	//加入分数文字
	addText();
	
	//加入事件
	backLayer.addEventListener(LMouseEvent.MOUSE_DOWN,onmousedown);
	backLayer.addEventListener(LEvent.ENTER_FRAME,onframe);
}

这个函数中我们首先初始化一些值,比如分数和游戏时间。在这里顺便说说这个游戏时间有什么用:因为我们的游戏随着时间的推进,难度应该越来越难,所以我们要保存下这个时间,方便以后的计算。

在startGame中,我们为了加入一个滚动的背景,我们用到了一个Background类。这个类代码如下:

/**
* Background.js
* @author Yorhom
* @date 2013/8/10/23:42
*/

function Background(){
	var s = this;
	base(s,LSprite,[]);
	
	//设置移动速度
	s.speed = 10;
	
	var skyBitmapData1 = new LBitmapData(datalist["sky.1"]);
	var skyBitmapData2 = new LBitmapData(datalist["sky.2"]);

	//记录每块背景的长度
	s.lastObjX = skyBitmapData1.image.width;
	
	//实例化第一块背景
	s.skyBitmap1 = new LBitmap(skyBitmapData1);
	//实例化第二块背景
	s.skyBitmap2 = new LBitmap(skyBitmapData2);
	//将第二块背景移动到第一块的后面
	s.skyBitmap2.x = s.lastObjX; 
	
	//加入两块背景
	s.addChild(s.skyBitmap1);
	s.addChild(s.skyBitmap2);
	
	//加入时间轴事件
	s.addEventListener(LEvent.ENTER_FRAME,s.run);
}
Background.prototype.run = function(s){
	//将背景向前移动
	s.skyBitmap1.x -= s.speed;
	s.skyBitmap2.x -= s.speed;
	
	//如果第一块背景移除到屏幕之外就移到另一块的后面
	if(s.skyBitmap1.x < -1 * s.lastObjX){
		s.skyBitmap1.x = s.skyBitmap2.x + s.lastObjX;
	}
	//如果第二块背景移除到屏幕之外就移到另一块的后面
	if(s.skyBitmap2.x < -1 * s.lastObjX){
		s.skyBitmap2.x = s.skyBitmap1.x + s.lastObjX;
	}
};
原理很简单,就是先将第二块背景接到第一块背景的后面,如果第一块背景已经移除屏幕了,那就把第一块背景加到第二块后面,以此类推,就实现了画面不段移动的感觉。

在startGame函数中,我们还用到了实例化层的函数initLayer(),这个函数代码如下:

function initLayer(){
	//加入飞机层
	plainLayer = new LSprite();
	backLayer.addChild(plainLayer);
	//加入敌机层
	enemyLayer = new LSprite();
	backLayer.addChild(enemyLayer);
	//加入子弹
	bulletLayer = new LSprite();
	backLayer.addChild(bulletLayer);
	//加入文字层
	textLayer = new LSprite();
	backLayer.addChild(textLayer);
}
另外还有个addText函数,负责显示分数文字用的。

function addText(){
	//实例化LTextField对象
	scoreText = new LTextField();
	scoreText.font = "Tekton Pro";
	scoreText.size = 20;
	scoreText.text = "Score: " + score;
	scoreText.x = LStage.width - scoreText.getWidth() - 20;
	scoreText.y = 20;
	//加到显示层中
	textLayer.addChild(scoreText);
}
还有就是在startGame中加的两个事件:时间轴事件,鼠标点击事件。鼠标事件触发的函数代码如下:

function onmousedown(event){
	//计算子弹飞出的角度
	var height = (player.y + player.getHeight()*0.5) - event.offsetY;
	var width = event.offsetX - (player.x + player.getWidth()*0.5);
	var angle = Math.atan2(height,width);
	//实例化一个子弹
	var bullet = new Bullet(angle);
	bulletLayer.addChild(bullet);
}
代码很简单,就是先取出当前点击位置离人物的宽度与高度,然后通过Math.atan2算出这个点与玩家飞机的直线距离和通过玩家飞机的水平直线的夹角度数,并将这个值当参数传入Bullet类中。Bullet类代码如下:

/**
* Bullet.js
* @author Yorhom
* @date 2013/8/12/21:14
*/

function Bullet(angle){
	var s = this;
	base(s,LSprite,[]);
	
	//计算子弹角度
	s._angle = angle * 180 / Math.PI;
	//保存子弹移动速度
	s._speed = 10;
	//保存当前子弹到玩家飞机的距离
	s._r = 0;
	
	//计算出初始位置
	s.x = player.x + player.getWidth()*0.5;
	s.y = player.y + player.getHeight()*0.5;
	
	//保存初始位置
	s._startX = s.x;
	s._startY = s.y;

	//添加子弹对象
	var bitmapData = new LBitmapData(datalist["bullet"]);
	s.bitmap = new LBitmap(bitmapData);
	s.addChild(s.bitmap);
	
	//添加射击时的音频对象
	var attackMusic = new LSound("./music/attack.wav");
	attackMusic.play();
}
Bullet.prototype.onframe = function(){
	var s = this;
	//更改当前子弹到玩家飞机的距离
	s._r += s._speed;
	
	//计算y轴移动距离
	var speedy = Math.sin(s._angle * Math.PI / 180) * s._r;
	//计算x轴移动距离
	var speedx = Math.cos(s._angle * Math.PI / 180) * s._r;
	//更改子弹位置
	s.x = s._startX + speedx;
    s.y = s._startY - speedy;
};
这个类主要负责显示一个子弹,并且让子弹往点击的方向飞去。显示一个子弹就是用一个LBitmap来实现。移动子弹的原理就是先把子弹到玩家飞机的直线距离设置为0,然后每当要移动子弹时,就将这个距离先加上移动速度,找到要到的位置,然后通过传进来的那个角度参数配合Math.cos和Math.sin算出要到的位置的x,y坐标,然后让子弹移动到那个位置上去。这个对于大伙儿应该很简单,但对于我这个只有初二水平的学生来说,连cos和sin都没学过,查了很多资料才搞出来的。

上面还提到了时间轴事件,触发的函数如下:

function onframe(){
	//增加游戏时间
	gameTime ++;
	//添加敌人
	if(frameIndex > maxFrame){
		var enemy = new Plain();
		enemyLayer.addChild(enemy);
		frameIndex = 0;
	}else{
		frameIndex ++;
	}
	//移除敌人
	for(var key in enemyLayer.childList){
		if(enemyLayer.childList[key].mode == "die"){
			enemyLayer.removeChild(enemyLayer.childList[key]);
			//增加分数
			score += 10;
			//显示新分数
			changeText();
			return;
		}
		if(enemyLayer.childList[key].mode == "complete"){
			gameOver();
			enemyLayer.removeChild(enemyLayer.childList[key]);
		}
	}
	//移除飞出屏幕的子弹
	for(var key in bulletLayer.childList){
		bulletLayer.childList[key].onframe();
		if(
			bulletLayer.childList[key].x > LStage.width
			|| bulletLayer.childList[key].x < 0
			|| bulletLayer.childList[key].y < 0
			|| bulletLayer.childList[key].y > LStage.height
		){
			bulletLayer.removeChild(bulletLayer.childList[key]);
		}
	}
}
每段代码都加了注释,结合API文档看一些就能明白的。其中有个Plain类,这个是一个用来实现敌机的类,包括敌机移动,检测碰撞等,代码如下:

/**
* Plain.js
* @author Yorhom
* @date 2013/8/15/12:10
*/

function Plain(){
	var s = this;
	base(s,LSprite,[]);
	
	//设置飞机移动速度
	s.speed = Math.floor(gameTime/100) + 7;
	s.mode = "";
	
	//添加敌人的图片
	var bitmapData = new LBitmapData(datalist["monster"]);
	s._bitmap = new LBitmap(bitmapData);
	s.x = LStage.width + s._bitmap.getWidth();
	s.y = Math.floor(Math.random()*(LStage.height-s._bitmap.getHeight()));
	s.addChild(s._bitmap);
	
	//通过时间轴事件实现不断移动
	s.addEventListener(LEvent.ENTER_FRAME,s.run);
}
Plain.prototype.run = function(s){
	//移动飞机对象
	s.x -= s.speed;
	
	//检测碰撞
	s.checkHit();
	
	//判断是否移除屏幕。如果是,就将mode属性设置为"complete"
	if(s.x < -1 * s.getWidth()){
		s.mode = "complete";
	}
};
Plain.prototype.checkHit = function(){
	var s = this;
	
	//判断碰撞
	for(var key in bulletLayer.childList){
		if(LStage.hitTestArc(s,bulletLayer.childList[key])){
			//将mode属性改为"die"
			s.mode = "die";
			//移除碰撞子弹
			bulletLayer.removeChild(bulletLayer.childList[key]);
		}
	}
};

可以在上面的构造器代码中看到,我们通过游戏时间变量gameTime计算了飞机移动的速度,达到改变游戏的难度。其他的代码就直接看注释和API文档就能看懂。

实现了这个类,我们的游戏基本上就搞定了。不过还有些细节部分不可忽视。

为了移除一些对象避免效率低下,我们在onframe中加入了移除对象的功能。为了实现这个功能,我们遍历了每个飞机对象,然后判断遍历到的飞机对象的mode属性是否为die,如果是,就移除掉。在onframe中,实现这个效果的代码如下:

//移除敌人
for(var key in enemyLayer.childList){
	if(enemyLayer.childList[key].mode == "die"){
		enemyLayer.removeChild(enemyLayer.childList[key]);
		//增加分数
		score += 10;
		//显示新分数
		changeText();
		return;
	}
	if(enemyLayer.childList[key].mode == "complete"){
		gameOver();
		enemyLayer.removeChild(enemyLayer.childList[key]);
	}
}
//移除飞出屏幕的子弹
for(var key in bulletLayer.childList){
	bulletLayer.childList[key].onframe();
	if(
		bulletLayer.childList[key].x > LStage.width
		|| bulletLayer.childList[key].x < 0
		|| bulletLayer.childList[key].y < 0
		|| bulletLayer.childList[key].y > LStage.height
	){
		bulletLayer.removeChild(bulletLayer.childList[key]);
	}
}

为了及时更改分数,我们在时间轴事件中还加入了调用changeText函数。代码如下:

function changeText(){
	//更改显示文字
	scoreText.text = "Score: " + score;
	//更改文字坐标
	scoreText.x = LStage.width - scoreText.getWidth() - 20;
}
还有就是游戏结束时调用的代码,如下:

function gameOver(){
	//消除事件
	backLayer.die();
	
	var bitmap;
	//加入游戏结束层
	var gameOverLayer = new LSprite();
	backLayer.addChild(gameOverLayer);
	//加入背景
	bitmap = new LBitmap(new LBitmapData(datalist["over_bg"]));
	gameOverLayer.addChild(bitmap);
	//加入文字
	bitmap = new LBitmap(new LBitmapData(datalist["over_text"]));
	bitmap.x = (LStage.width - bitmap.getWidth()) * 0.5;
	bitmap.y = (LStage.height - bitmap.getHeight()) * 0.5;
	gameOverLayer.addChild(bitmap);
	
	//将游戏结束层移除屏幕
	gameOverLayer.y = -1 * gameOverLayer.getHeight();
	//通过缓动将游戏结束层移到屏幕上
	LTweenLite.to(gameOverLayer,0.7,{
		y:0,
		ease:Quad.easeInOut,
		onComplete:function(){
			//加入鼠标事件,来应对游戏重开
			backLayer.addEventListener(LMouseEvent.MOUSE_DOWN,restart);
		}
	});
	
	//关掉游戏进行中的音乐
	playingMusic.close();
	//播放游戏结束时候的音乐
	dieMusic.play(0,1);
	overMusic.play(0,100000000000000000000000000000000);
}
最后就是游戏重开函数:

function restart(){
	//清除界面
	backLayer.die();
	backLayer.removeAllChild();
	//关掉游戏结束时候的音乐
	overMusic.close();
	//开始游戏
	startGame();
}

上面基本上把整个游戏制作过程简略地讲了一遍,代码讲解有点不详细,大家可以结合注释看看。另外如果有感兴趣的朋友,可以到下面链接里下载。下载包里还有我打包好的apk文件,大家可以在手机上玩玩。打包apk的话,可以看看这篇文章:《用HTML5来开发一款android本地化App游戏-宝石碰碰》

源代码下载地址:http://files.cnblogs.com/yorhom/SpaceWar.rar


结合Qt实现本地化EXE游戏

下载和安装Qt等一些基础的东西这里就不多说了,Google一下或者百度一下就可以了。接下来就直接讲方法。

首先你需要在Qt Creater中创建一个Qt项目,创建项目的方法如下。

首先点开File->New File or Project,出现以下对话框,选择如图所示的几个选项:


点击Choose...按钮,进入如下界面:


上面的信息随便添就可以。点击Next,出现如下界面:


上面是在选择配置,根据自己下载的选择一些就ok,点击Next继续。出现如下界面


按照上面的添法填写好后,再继续按下Next,进入下一个界面。


然后按下Finish就已经创建好项目了。得到以下的目录树,大家可以看看操作对没有:


然后把装有html5游戏的文件夹复制到执行文件目录下。如下图:


注意:貌似游戏加了音乐就会运行不出来了,把有用到音乐的地方全部删掉就ok没事了

打开mainwindow.h,写入以下代码:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class MainWindow : public QMainWindow
{
	Q_OBJECT
	
public:
	MainWindow(QWidget *parent = 0);
	~MainWindow();
};

#endif // MAINWINDOW_H

接着在mianwindow.cpp加入以下代码:

#include <QtWebKit/QWebView>
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
	: QMainWindow(parent)
{
	setWindowTitle(QString(""));
	setMaximumSize(QSize(800, 480));
	setMinimumSize(QSize(800, 480));
	showMaximized();
	setWindowIcon(QIcon("./SpaceWar/images/logo.jpg"));

	QWebView *pWebView = new QWebView(this);
	setCentralWidget(pWebView);

	pWebView->load("file:///"+QUrl(QFileInfo("./SpaceWar/index.html").absoluteFilePath()));
}

MainWindow::~MainWindow()
{
	
}

注意,QUrl里面的路径要是绝对路径,并且要在取出的绝对路径前面加上file:///。

再打开main.cpp,加入以下代码:

#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
	QApplication a(argc, argv);
	MainWindow w;
	w.show();
	
	return a.exec();
}

按下如图所示的按钮运行程序:


运行效果如下:


最后要发布的时候,要找到几个dll,如下:

  1. libgcc_s_dw2-1.dll
  2. libstdc++-6.dll
  3. QtCore4.dll
  4. QtGui4.dll
  5. QtNetwork4.dll
  6. QtWebKit4.dll
把这几个dll放在执行文件目录下面,就可以了。

如下图所示:


over,exe就打包完成了。是不是很简单?

本次讲解就到这里了,欢迎大家捧场~~支持就是最大的鼓励!

如果文中有疏漏的地方或者大家有任何疑问都欢迎在文章下面留言。


----------------------------------------------------------------

欢迎大家转载我的文章。

转载请注明:转自Yorhom's Game Box

http://blog.csdn.net/yorhomwang

欢迎继续关注我的博客

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页