前几天公司要做一个拼图的小游戏,我看到了《速度挑战 - 2小时完成HTML5拼图小游戏》,照着写完了一个小游戏以后,这几天使用cropper和lufylegend游戏引擎制作了一款简单的可以自定义图片的拼图华容道游戏,该游戏除了实现基本的游戏功能以外,还支持游戏图片上传,剪切,以及图片过大可以进行压缩的功能。
准备
俗话说“兵马未动粮草先行”,在直接开始撸游戏之前,需要先做一些准备:
(1)cropper
cropper是一款使用简单且功能强大的图片剪裁jQuery插件,我选择使用该插件来实现图片裁剪的功能,在使用之前需要引入cropper:
<link href="https://cdn.bootcss.com/cropperjs/1.3.6/cropper.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/cropperjs/1.3.6/cropper.min.js"></script>
(2)jQuery
jQuery就不多说了,cropper就是jQuery的插件,自然需要引入,需要注意的是jQuery需要在cropper之前引入
(3)lufylegend
lufylegend是一个HTML5开源引擎,使用之前需要引入:
<script src="js/lib/lufylegend-1.10.1.simple.min.js"></script>
(4)WeUI
WeUI是一套同微信原生视觉体验一致的基础样式库,由微信官方设计团队为微信内网页和微信小程序量身设计,令用户的使用感知更加统一。使用这个ui库主要是使用了gallery
用来展示上传的图片用于下一步的裁剪,要使用的话也需要引入:
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui.min.css">
<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.1.3/weui.min.js"></script>
开始
在做好之前的准备以后,就可以开始撸游戏了
1.布局
游戏本身只需要一个div
,整个页面分为两块,图片上传模块和游戏模块,整体还是比较简单,之后可以继续增加其他的内容:
<input type="file" id="file" multiple="multiple" accept="image/*" style="display: none">
<!--图片上传按钮-->
<div class="weui-uploader__input-box getImg">
<input id="getImg" class="weui-uploader__input">
</div>
<!--图片上传-->
<div id="uploadPhotoBox" class="page gallery js_show img" style="display: none">
<div class="weui-gallery" style="display: block">
<div id="photoBox" class="photo weui-gallery__img">
<img id="photo" style="max-width: 100%" src="">
</div>
<div class="tool">
<a>
<i id="crop">确定</i>
<i id="cancel">取消</i>
</a>
</div>
</div>
</div>
<!--游戏-->
<div class="game" style="display: none">
<div class="time">时间: <span id="time">99</span></div>
<div class="steps">步数: <span id="steps">0</span></div>
<div id="myGame"></div>
</div>
还有一点点样式:
.time, .steps {
position: absolute;
margin-left: 20%;
margin-top: 5%;
}
.steps {
margin-top: 10%;
}
.tool {
position: absolute;
z-index: 2;
background-color: #111111;
width: 100%;
}
.tool i {
font-size: 1rem;
color: white;
right: 0;
float: right;
padding-left: 2rem;
padding-right: 2rem;
}
#cancel {
float: left;
}
2.定义全局变量
首先需要定义一些变量,并且添加一些简单的逻辑事件,例如鼠标点击:
var imgResult;//裁剪出来图片的base64值
var imgResultImg = new Image();//裁剪出来的图片对象
var flObj = document.getElementById("file");
$('#getImg').click(function () {
$('#file').bind('change', function () {//文件上传控件绑定监听事件
var file = $(this).val();
if (file.length > 0) {//文件不为空时自动提交图片
uploadImg();//图片提交
$('#photoBox').css('display', 'block');
$('.getImg').css('display', 'none');
}
});
$('#file').click();
$('#uploadPhotoBox').css('display', 'block');
});
$('#cancel').click(function () {//取消图片提交
window.location.reload();
});
上面的uploadImg()
函数会在之后有定义
3.图片处理部分
需要完成图片的上传,压缩,裁剪功能
3.1 图片上传
在用户选择文件以后要先判断是否是图片,然后再判断图片大小确定是否要压缩,然后才能开始裁剪
function uploadImg() {//图片上传
$('#photoBox').empty();
$('#photoBox').html('<img id="photo" src="">');
var file = flObj.files[0];//因为每次只上传了一张图片,所以获取到flObj.files[0];
var fReader = new FileReader();
var isImage = checkFile(file);//检查文件是否为图像类型
if (!isImage) {
alert("请确保文件为图像类型");
} else {
fReader.onload = function (e) {
var imageSize = e.total; //图片大小
var image = new Image();
image.src = e.target.result;
image.onload = function () {
//判断是否需要压缩图片
image = judgeCompress(image, imageSize);
document.getElementById("photo").src = image.src;
cropper(document.getElementById("photo"), options);
};
}
}
fReader.readAsDataURL(isImage);
};
检查文件是否为图像类型:
function checkFile(file) {//检查文件是否为图像类型
console.log(file);
//使用正则表达式匹配判断
if (!/image\/\w+/.test(file.type)) {
return false;
}
return file;
}
3.2 图片压缩
在图片压缩之前,需要先检查图片大小,图片过大才需要压缩:
function judgeCompress(image, imageSize) {//判断图片大小
//判断图片是否大于300000 bit
var threshold = 300000; //阈值,可根据实际情况调整
if (imageSize > threshold) {
var imageData = compress(image);//图片压缩
var newImage = new Image();
newImage.src = imageData;
return newImage;
} else {
return image;
}
}
图片压缩是使用canvas
实现,先将图片绘制出来,然后再讲绘制出来的图片保存为图片对象以完成压缩,代码实现如下:
function compress(image) {//图片压缩
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var originWidth = image.width;
var originHeight = image.height;
var maxWidth = 800,
maxHeight = 800;
// 目标尺寸
var targetWidth = originWidth,
targetHeight = originHeight;
// 图片尺寸超过800x800的限制
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > maxWidth / maxHeight) {
// 更宽,按照宽度限定尺寸
targetWidth = maxWidth;
targetHeight = Math.round(maxWidth * (originHeight / originWidth));
} else {
targetHeight = maxHeight;
targetWidth = Math.round(maxHeight * (originWidth / originHeight));
}
}
canvas.height = targetHeight;
canvas.width = targetWidth;
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
//压缩操作
var quality = 0.8; //图片质量 范围:0<quality<=1 根据实际需求调正
var imageData = canvas.toDataURL("image/jpeg", quality);
return imageData;
}
3.3 图片裁剪
得到了处理完成的图片以后,就可以进行图片裁剪了,首先先按照官网进行插件的配置,我把它们写在了一个对象中:
var options = {
aspectRatio: 1, //宽高比
preview: '.preview', //预览窗口
guides: true, //裁剪框的虚线
autoCropArea: 0.5, //0-1之间的数值,定义自动剪裁区域的大小,默认0.8
dragCrop: true, //是否允许移除当前的剪裁框,并通过拖动来新建一个剪裁框区域
movable: true, //是否允许移动剪裁框
resizable: true, //是否允许改变裁剪框的大小
zoomable: false, //是否允许缩放图片大小
mouseWheelZoom: false, //是否允许通过鼠标滚轮来缩放图片
touchDragZoom: false, //是否允许通过触摸移动来缩放图片
rotatable: false, //是否允许旋转图片
minContainerWidth: 200, //容器的最小宽度
minContainerHeight: 200, //容器的最小高度
minCanvasWidth: 0, //canvas 的最小宽度(image wrapper)
minCanvasHeight: 0, //canvas 的最小高度(image wrapper)
strict: true,
};
然后就是图片这部分最核心的裁剪部分了,但是使用cropper裁剪出来的图片是canvas
,解决的方法是将canvas
使用toDataURL()
方法转化为base64
之后再转为img
对象,用于后面的操作:
function cropper(photo, options) {//图片裁剪
var cropper = new Cropper(photo, options);
$('#crop').on('click', function () {
imgResult = cropper.getCroppedCanvas().toDataURL();//裁剪出来的base64
imgResultImg.src = imgResult;//裁剪出来的图片对象
$('.img').css('display', 'none');
LInit(60, "myGame", gameWidth, gameHeight, main);//游戏初始化
$('.game').css('display', 'block');
})
}
这样子图片处理部分就完成了,在图片裁剪完成之后就开始初始化游戏了。
4.游戏部分
接下来就开始游戏部分的制作,游戏中大致思路模仿了《速度挑战 - 2小时完成HTML5拼图小游戏》,在其基础上做了修改
4.1 定义变量
首先还是要定义游戏中需要用到的变量:
/** 初始化游戏 */
var gameWidth = 390;
var gameHeight = 390;
/** 游戏层 */
var stageLayer, gameLayer, overLayer;
/** 拼图块列表 */
var blockList;
/** 是否游戏结束 */
var isGameOver,
isTimeOver;
/** 用时 */
var startTime, time, countTime;
/** 步数 */
var steps;
/** 图片 */
var imgBmpd, startNewGame, fail, startBitmap, Again, failBitmap, againBitmap, succeed, succeedBitmap;
var _blockList = [];//拼图序列
var datalist = [];//存放图片
4.2 游戏初始化
游戏初始化,包括素材的加载,以及游戏界面的显示:
function main() {//游戏资源初始化
/** 全屏设置 */
if (LGlobal.mobile) {
LGlobal.width = gameWidth;
LGlobal.height = gameHeight;
LGlobal.stageScale = LStageScaleMode.SHOW_ALL;
}
LGlobal.screen(LGlobal.FULL_SCREEN);
LGlobal.preventDefault = false;
/** 添加加载提示 */
var loadingHint = new LTextField();
loadingHint.text = "资源加载中……";
loadingHint.size = 20;
loadingHint.x = (LGlobal.width - loadingHint.getWidth()) / 2;
loadingHint.y = (LGlobal.height - loadingHint.getHeight()) / 2;
addChild(loadingHint);
/** 加载图片 文件*/
LLoadManage.load(
[
{path: "./js/Block.js"},
{name: "startGame", path: "./images/start.png"},
{name: "fail", path: "./images/fail.png"},
{name: "Again", path: "./images/challengeAgain.png"},
{name: "succeed", path: "./images/succeed.png"},
],
null,
function (result) {
/** 移除加载提示 */
loadingHint.remove();
/** 保存位图数据,方便后续使用 */
imgBmpd = new LBitmapData(imgResultImg);
gameInit(result);
}
);
}
function gameInit(e) {//游戏内容初始化
datalist = e;
var bitmapData = new LBitmapData(imgResultImg);
var bitmap = new LBitmap(bitmapData);
bitmap.scaleX = LGlobal.width / bitmap.width;
bitmap.scaleY = LGlobal.height / bitmap.height;
bitmap.width = LGlobal.width;
bitmap.height = LGlobal.height;
bitmap.x = 0;
bitmap.y = 0;
addChild(bitmap);
startNewGame = new LBitmapData(datalist['startGame']);
fail = new LBitmapData(datalist["fail"]);
Again = new LBitmapData(datalist["Again"]);
succeed = new LBitmapData(datalist["succeed"]);
startBitmap = new LBitmap(startNewGame);
failBitmap = new LBitmap(fail);
againBitmap = new LBitmap(Again);
succeedBitmap = new LBitmap(succeed);
/** 初始化舞台层 */
stageLayer = new LSprite();
stageLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "transparent");
addChild(stageLayer);
/** 初始化游戏层 */
gameLayer = new LSprite();
stageLayer.addChild(gameLayer);
/** 初始化最上层 */
overLayer = new LSprite();
stageLayer.addChild(overLayer);
/** 添加开始界面 */
addBeginningUI();
}
function addBeginningUI() {//游戏开始界面
var beginningLayer = new LSprite();
beginningLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "transparent");
stageLayer.addChild(beginningLayer);
/** 游戏标题 */
var title = new LTextField();
title.text = "拼图华容道";
title.size = 48;
title.weight = "bold";
title.x = (LGlobal.width - title.getWidth()) / 2;
title.y = 70;
title.color = "#f8fbb5";
title.lineWidth = 5;
title.lineColor = "#000000";
title.stroke = true;
beginningLayer.addChild(title);
/** 开始游戏提示 */
startBitmap.scaleX = 0.7;
startBitmap.scaleY = 0.7;
startBitmap.x = (LGlobal.width - startBitmap.getWidth()) / 2;
startBitmap.y = 250 + 0;
beginningLayer.addChild(startBitmap);
/** 初始化拼图块列表 */
initBlockList();
/** 打乱拼图 */
getRandomBlockList();
/** 开始游戏 */
beginningLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
beginningLayer.remove();
startGame();
});
}
4.3 游戏主体
接下来来到了整个游戏最重要的部分,游戏主体的实现,开始以后将拼图打乱,然后进行游戏:
(1)开始游戏
点击界面以后将游戏的各种值初始化,然后开始游戏
function startGame() {//开始游戏
isGameOver = false;
isTimeOver = false;
/** 初始化时间和步数 */
startTime = (new Date()).getTime();
countTime = 0;
time = 0;
steps = 0;
/** 显示拼图 */
showBlock();
/** 计时 */
updateTimeTxt(countTime);
/** 显示步数 */
updateStepsTxt();
stageLayer.addEventListener(LEvent.ENTER_FRAME, onFrame);
}
(2)显示拼图
显示拼图显然不是一张图,那么就需要拼图块,后面会定义一个类来表示这些拼图块
function showBlock() {//显示拼图
for (var i = 0, l = blockList.length; i < l; i++) {
var b = blockList[i];
/** 根据序号计算拼图块位置 */
var y = (i / 3) >>> 0, x = i % 3;
b.setLocation(x, y);
gameLayer.addChild(b);
}
}
(3)初始化拼图列表
根据序号计算拼图块图片显示位置,将拼图块存放到列表中
function initBlockList() {//初始化拼图列表
blockList = new Array();
for (var i = 0; i < 9; i++) {
/** 根据序号计算拼图块图片显示位置 */
var y = (i / 3) >>> 0, x = i % 3;
blockList.push(new Block(i, x, y));
}
}
(4)打乱拼图
这里使用的是随机打乱拼图,然后计算序列的倒序和,如果倒序和是奇数,拼图无法完成,需要重新打乱,直到倒序和为偶数。
function getRandomBlockList() {//随机打乱拼图
/** 随机打乱拼图 */
blockList.sort(function () {
return 0.5 - Math.random();
});
/** 计算逆序和 */
var reverseAmount = 0;
for (var i = 0, l = blockList.length; i < l; i++) {
var currentBlock = blockList[i];
for (var j = i + 1; j < l; j++) {
var comparedBlock = blockList[j];
if (comparedBlock.index < currentBlock.index) {
reverseAmount++;
}
}
}
/** 检测打乱后是否可还原 */
if (reverseAmount % 2 != 0) {
/** 不合格,重新打乱 */
getRandomBlockList();
} else {
var str = "";
for (var i = 0; i < blockList.length; i++) {
str = str + blockList[i].index;
}
console.log(str);
if (str.substr(0, 3) == "012") {
getRandomBlockList();
} else {
_blockList.push(str);
}
}
}
4.4 拼图块
在拼图过程中,引入了一个新的Block
类,这个类用来表示并且操作拼图块:
function Block(index, x, y) {
LExtends(this, LSprite, []);
var bmpd = imgBmpd.clone();
if (index != 8) {
bmpd.setProperties(x * bmpd.width / 3, y * bmpd.width / 3, bmpd.width / 3, bmpd.width / 3);
this.bmp = new LBitmap(bmpd);
this.bmp.scaleX = 130 / this.bmp.width;
this.bmp.scaleY = 130 / this.bmp.height;
this.addChild(this.bmp);
} else {
var shape = new LShape();
shape.graphics.drawRect(2, "#ffffff", [0, 0, 130, 130], true, "#ffffff");
this.addChild(shape);
}
// 格子边框
var border = new LShape();
border.graphics.drawRect(3, "#ffffff", [0, 0, 130, 130]);
border.graphics.drawRoundRect(3, "#ffffff", [0, 0, 130, 130, 10]);
this.addChild(border);
this.index = index;
this.addEventListener(LMouseEvent.MOUSE_UP, this.onClick);
}
Block.getBlock = function (x, y) {
return blockList[y * 3 + x];
};
Block.isGameOver = function () {
var reductionAmount = 0, l = blockList.length;
/** 计算还原度 */
for (var i = 0; i < l; i++) {
var b = blockList[i];
if (b.index == i) {
reductionAmount++;
}
}
/** 计算是否完全还原 */
if (reductionAmount == l) {
/** 游戏结束 */
gameOver();
}
};
Block.exchangePosition = function (b1, b2) {
var b1x = b1.locationX, b1y = b1.locationY,
b2x = b2.locationX, b2y = b2.locationY,
b1Index = b1y * 3 + b1x,
b2Index = b2y * 3 + b2x;
/** 在地图块数组中交换两者位置 */
blockList.splice(b1Index, 1, b2);
blockList.splice(b2Index, 1, b1);
/** 交换两者显示位置 */
b1.setLocation(b2x, b2y);
b2.setLocation(b1x, b1y);
/** 判断游戏是否结束 */
Block.isGameOver();
};
Block.prototype.setLocation = function (x, y) {//方块位置
this.locationX = x;
this.locationY = y;
this.x = x * 130;
this.y = y * 130 + 0;
};
Block.prototype.onClick = function (e) {//方块的点击事件
var self = e.currentTarget;
if (isGameOver) {
return;
}
var checkList = new Array();
/** 判断右侧是否有方块 */
if (self.locationX > 0) {
checkList.push(Block.getBlock(self.locationX - 1, self.locationY));
}
/** 判断左侧是否有方块 */
if (self.locationX < 2) {
checkList.push(Block.getBlock(self.locationX + 1, self.locationY));
}
/** 判断上方是否有方块 */
if (self.locationY > 0) {
checkList.push(Block.getBlock(self.locationX, self.locationY - 1));
}
/** 判断下方是否有方块 */
if (self.locationY < 2) {
checkList.push(Block.getBlock(self.locationX, self.locationY + 1));
}
for (var i = 0, l = checkList.length; i < l; i++) {
var checkO = checkList[i];
/** 判断是否是空白拼图块 */
if (checkO.index == 8) {
steps++;
updateStepsTxt();
Block.exchangePosition(self, checkO);
var str = "";
for (var i = 0; i < blockList.length; i++) {
str = str + blockList[i].index;
}
_blockList.push(str);
break;
}
}
};
4.5 游戏结束
添加游戏结束功能,根据拼图完成或者计时结束判断游戏成功或是失败
(1)游戏计时计步
游戏未结束之前更新游戏的时间和步数:
function onFrame() {//计时
if (isGameOver) {
return;
}
if (isTimeOver) {
return;
}
/** 获取当前时间 */
var currentTime = (new Date()).getTime();
/** 计算使用的时间并更新时间显示 */
time = currentTime - startTime;
if (countTime > 0) {// 倒计时
updateTimeTxt();
} else {
timeOver();
}
}
(2)更新时间和步数
function updateTimeTxt() {//更新时间
$('#time').html(getTimeTxt());
}
function getTimeTxt() {
var d = new Date(time);
countTime = 99 - Math.floor(d / 1000);
return countTime;
}
function updateStepsTxt() {//更新步数
$('#steps').html(steps);
}
(3)游戏失败
游戏时间结束后视为游戏失败,弹出游戏失败界面,点击重新开始
function timeOver() {// 判断时间是否结束 失败
isTimeOver = true;
var resultLayer = new LSprite();
resultLayer.filters = [new LDropShadowFilter()];
resultLayer.graphics.drawRoundRect(3, "#BBBBBB", [0, 0, 390, 450, 5], true, "rgba(0,0,0,.6)");
resultLayer.x = (LGlobal.width - resultLayer.getWidth()) / 2;
resultLayer.y = LGlobal.height / 2;
resultLayer.alpha = 0;
overLayer.addChild(resultLayer);
failBitmap.scaleX = 0.6;
failBitmap.scaleY = 0.6;
failBitmap.x = (LGlobal.width - failBitmap.getWidth()) / 2;
failBitmap.y = 70 + 0;
resultLayer.addChild(failBitmap);
againBitmap.scaleX = 0.6;
againBitmap.scaleY = 0.6;
againBitmap.x = (LGlobal.width - againBitmap.getWidth()) / 2;
againBitmap.y = 250 + 0;
resultLayer.addChild(againBitmap);
LTweenLite.to(resultLayer, 0.5, {
alpha: 0.7,
y: (LGlobal.height - resultLayer.getHeight()) / 2 - 15,
onComplete: function () {
/** 点击界面重新开始游戏 */
stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
gameLayer.removeAllChild();
overLayer.removeAllChild();
stageLayer.removeAllEventListener();
main();
});
}
});
}
(4)游戏成功
拼图完成时游戏成功,弹出游戏成功界面,输出游戏用时,步数,分数以及游戏情况的序列,点击重新开始
function gameOver() {// 游戏成功
let score = 99 - getTimeTxt(time);
let blockList = _blockList.join("-");
let step = steps;
isGameOver = true;
console.log('用时:' + getTimeTxt(time) + '步数:' + step + '分数:' + score + '序列:' + blockList);
var resultLayer = new LSprite();
resultLayer.filters = [new LDropShadowFilter()];
resultLayer.graphics.drawRoundRect(3, "#BBBBBB", [0, 0, 390, 450, 5], true, "rgba(0,0,0,.6)");
resultLayer.x = (LGlobal.width - resultLayer.getWidth()) / 2;
resultLayer.y = LGlobal.height / 2;
resultLayer.alpha = 0;
overLayer.addChild(resultLayer);
succeedBitmap.scaleX = 0.6;
succeedBitmap.scaleY = 0.6;
succeedBitmap.x = (LGlobal.width - succeedBitmap.getWidth()) / 2;
succeedBitmap.y = 70 + 0;
resultLayer.addChild(succeedBitmap);
againBitmap.scaleX = 0.6;
againBitmap.scaleY = 0.6;
againBitmap.x = (LGlobal.width - againBitmap.getWidth()) / 2;
againBitmap.y = 250 + 0;
resultLayer.addChild(againBitmap);
LTweenLite.to(resultLayer, 0.5, {
alpha: 0.7,
y: (LGlobal.height - resultLayer.getHeight()) / 2 - 15,
onComplete: function () {
/** 点击界面重新开始游戏 */
stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
gameLayer.removeAllChild();
overLayer.removeAllChild();
stageLayer.removeAllEventListener();
main();
});
}
});
}
反思和总结
感受
终于撸完了这个简单的小游戏,虽然写的毛毛糙糙,很多地方写得也不够好,但是收获肯定是有的,不光是看了文档,学习了大佬们的思路,同时也增长了一些动手能力,虽然代码比较简单,但是只要坚持,日积月累肯定会有收获。
可优化项
在写这篇博客的时候,回顾代码就已经满满的槽点了,把可优化项记录下来,以后有空可以尝试去优化:
- 1.使用jQuery和原生js两种方式获取dom元素,编码习惯不好,使代码更加混乱
- 2.游戏白底界面过于简单
- 3.随机打乱拼图,使游戏无法设置多种难度,缺少游戏乐趣
- 4.图片裁剪部分布局粗糙使用WeUI来完成
- 5.无法保留游戏记录