结对编程作业
https://github.com/WangYezhen/Piggytail
姓名 | 分工 | 博客链接 |
---|---|---|
王业震 | 后端逻辑实现以及ai算法的设计 | https://www.cnblogs.com/KleinWang/p/15455947.html |
张静 | 原型设计以及前端实现 | https://www.cnblogs.com/mingliangzi/p/15455974.html |
猪尾巴已经在服务器上部署完成,首先呈上试玩域名:bngel.cn,但由于校园网网速较慢,可能会出现无法登录的问题,欢迎大家访问,并提出宝贵的意见😊!!
一、原型设计
利用原型设计工具墨刀进行原型设计。
具体设计如下:
1️⃣游戏首页界面:由三个按钮组成联网对战,人机对战,双人对战,可由玩家进行模式选择。
2️⃣若用户选择联网模式则弹出输入窗口,成功输入学号姓名后进入游戏大厅
3️⃣游戏大厅界面:有两个按钮,其一是为加入房间,另一个为创建房间
若用户点击加入房间,则弹出弹框提供用户输入想加入的房间号
若用户点击创建房间,则自动生成房间的uuid并切换到联网对战界面
4️⃣对战界面,为了避免游戏界面单一,设计了三种对战界面
-
联网对战界面:左上角为当前房间号,label表示当前出牌玩家。将扑克分为三块区域,分别为牌库,放置区,玩家手牌区,并且将玩家出牌区细分为四个区域。页面中设置有托管按钮,玩家点击后可进入托管模式,再次点击取消托管按钮则退出托管。若玩家想退出则可点击退出按钮,返回上一级界面。
对局结束,弹窗展示房主/参与者获胜
-
人机对战界面
获胜界面,若机器人获胜时弹窗提示"机器人获胜"
-
双人对战界面
获胜界面,弹窗提示1P/2P玩家获胜
遇到的困难及解决方法
对于原型的设计选用墨刀是因为是参考了舍友的建议,后来和队友确定了大致的页面需求,对于墨刀的使用一开始非常迷茫,后来找到了墨刀官方的教程视频制作“打地鼠”,通过这个视频学会了怎么实现页面之间的交互,弹窗的实现以及定时器的使用。
在对墨刀上手之后,遇到的最大的困难就是页面的设计以及素材的查找,一开始设计将玩家出牌区整合在一块,但是发现玩家无法得知现有的花色,和队友讨论后决定将花色分开,并且对花色进行计数。
通过这次模型介绍,体会到了一定要先进行需求分析然后再进行原型设计,不然就要一直推翻之前所做的设计重新再来一遍。
二、原型设计实现
2.1 代码实现思路
2.2.1 网络接口的使用
本工程使用到的网络接口有登录接口、创建新对局、加入一个对局、执行玩家操作、获取上部操作以及获取对局信息。
我们使用的开发软件是Cocos Creator,开发语言是Javascript,直接使用new XMLHttpResquest() 即可创建一个连接对象。接着向连接对象发送Post/Get/Put请求即可实现需要的服务器连接任务。下面以登录接口为例进行解释说明。
let url = "http://172.17.173.97:8080/api/user/login";
let xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
let id = this.id.string;
let password = this.password.string;
let sendmessage = 'student_id=' + id + '&password=' + password;
xhr.send(sendmessage);
xhr.onreadystatechange = function () {
if (xhr.status == 200) {
var response = xhr.responseText;
response = JSON.parse(response);
cc.sys.localStorage.setItem("token",response.data.token);
cc.director.loadScene("gameCenter");
}
首先定义一个“xhr”连接对象,接着通过open()方法指定请求类型、请求url并通过setRequestHeader()方法设置请求头,最后通过send()方法将学号、密码等信息发送,在请求回调函数中接受返回信息,其中xhr.status == 200表示请求成功,将token作为共享变量,使用Cocos自带方法cc.sys.localStorage.setItem()保存供后续使用。若xhr.status不为200则表示请求失败,不会保存response与token。
2.1.2 代码组织与内部实现设计
下面以最复杂最核心的联网对战脚本进行讲解。
CocosCreator代码结构分为两部分,一是共享变量、二是cc.Class类。在cc.Class类中包括properties类、onload()界面初始化回调函数、start()界面开始回调函数、update()界面刷新回调函数。如下图所示:
2.1.3 说明算法的关键与关键实现部分流程图
(1)脚本函数
下面以函数数量最多的双人对战列举说明:
- start(): 界面开始回调函数,在这里我们对共享变量进行重赋值;
- tap_store(): 牌库翻牌按钮回调函数;
- tap_1c(): 1P从手牌中翻出梅花按钮回调函数;
- tap_2c(): 2P从手牌中翻出梅花按钮回调函数;
- tap_1d(): 1P从手牌中翻出方片按钮回调函数;
- tap_2d(): 2P从手牌中翻出方片按钮回调函数;
- tap_1h(): 1P从手牌中翻出红心按钮回调函数;
- tap_2h(): 2P从手牌中翻出红心按钮回调函数;
- tap_1s(): 1P从手牌中翻出黑桃按钮回调函数;
- tap_2s(): 2P从手牌中翻出黑桃按钮回调函数;
- tap_ok(): 游戏结束后弹窗获取游戏结果后退回至主界面按钮回调函数;
- tap_back(): 退出至主界面按钮回调函数。
(2)AI实现算法及算法图解
我们拟采用贪心算法来实现托管AI,即忽略扑克牌分布,仅保证现在手牌是最优状态,为使对手吃牌概率最大,我们将先打空手牌中数量最多的花色,且在出牌时要保证和不和放置区顶花色相同。算法可分为下面几个步骤:
-
当最大数量花色现有数为0时,启动最大数量花色重定向,锁定现有手牌中数量最大的花色,实现代码如下所示:
robot_start() { if (turn == true) { let robot = cc.find("Canvas/New Sprite(Splash)/robot").getComponent('gameonline'); if (value == 0) { let maxvalue = Math.max(card_c.length, card_d.length, card_h.length, card_s.length); if(maxvalue == card_c.length) {kind = 'C';} else if(maxvalue == card_d.length) {kind = 'D';} else if(maxvalue == card_h.length) {kind = 'H';} else {kind = 'S';} value = maxvalue; }
-
当放置区牌数为0时,优先打出上一步定位的最大数量花色,若最大数量花色为0,则从牌库中翻牌,实现代码如下所示:
if (layer.length == 0) { if (value == 0) { robot.tap_store(); } else { if(kind == 'C') {robot.tap_c(); value--;} else if(kind == 'D') {robot.tap_d(); value--;} else if(kind == 'H') {robot.tap_h(); value--;} else if(kind == 'S') {robot.tap_s(); value--;} }
-
当牌库放置区数不为0时,优先打出上一步定位的最大数量花色,若放置区顶与定位花色相同或最大数量花色为0,则从牌库中翻牌,实现代码如下所示:
} else { if (value == 0) { robot.tap_store(); } else { if(kind == 'C' && layer[layer.length-1][0]!='C') {robot.tap_c(); value--;} else if(kind == 'D' && layer[layer.length-1][0]!='D') {robot.tap_d(); value--;} else if(kind == 'H' && layer[layer.length-1][0]!='H') {robot.tap_h(); value--;} else if(kind == 'S' && layer[layer.length-1][0]!='S') {robot.tap_s(); value--;} else { if (card_c.length!=0 && layer[layer.length-1][0]!='C') {robot.tap_c();} else if (card_d.length!=0 && layer[layer.length-1][0]!='D') {robot.tap_d();} else if (card_h.length!=0 && layer[layer.length-1][0]!='H') {robot.tap_h();} else if (card_s.length!=0 && layer[layer.length-1][0]!='S') {robot.tap_s();} else {robot.tap_store();} } } } } },
AI算法流程图如下所示:
2.1.4 贴出你认为重要的/有价值的代码并解释
下面是请求刷新回调函数loopcallback()部分代码展示,其为联机对战的核心代码,作用是监听对手是否出牌,当对手出牌后则停止刷新,等待我方出牌。在我方出牌后,开启刷新,监听对手出牌状态。
window.loopcallback = function() {
let self = this;
let uuid=cc.sys.localStorage.getItem("uuid");
cc.log(uuid)
let url = "http://172.17.173.97:9000/api/game/" + uuid + "/last";
let token=cc.sys.localStorage.getItem("token");
let xhr1 = new XMLHttpRequest();
xhr1.open("GET", url, true);
xhr1.setRequestHeader("Authorization", token);
xhr1.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
xhr1.send();
xhr1.onreadystatechange = function () {
cc.log('正在刷新');
if (xhr1.status == 200) {
var response = xhr1.responseText;
response = JSON.parse(response);
if (begin != '对局刚开始' && response.data.last_msg == '对局刚开始') {// 刚刚进入战局
begin = response.data.last_msg;
turn = response.data.your_turn;
if(turn == false) {
cc.find("Canvas/New Sprite(Splash)/turn_label").getComponent(cc.Label).string="对方回合";
firstsch = false;
} else {
cc.find("Canvas/New Sprite(Splash)/turn_label").getComponent(cc.Label).string="你的回合";
firstsch = true;
}
} else if(response.data.hasOwnProperty('your_turn')) {
let response_store = response;
cc.log("response: ", response_store);
let turn_store = response_store.data.your_turn;
cc.log("turn: ",turn);
cc.log("turn_store: ",turn_store);
if (turn_store != turn) {// 下一轮已开始
if(turn_store == true) {
cc.log('unschedule')
self.unschedule(loopcallback);
cc.find("Canvas/New Sprite(Splash)/turn_label").getComponent(cc.Label).string="你的回合";
turn = turn_store;
let operation = response_store.data.last_code.split(' ');
let type = operation[1];
let card_name = operation[2];
let card_kind = card_name[0];
if (type == '0') {
let card_now = cc.find("Canvas/New Sprite(Splash)/"+card_name);
card_now.active = true;
card_now.zIndex = epoch++;
card_now.x = 99;
card_now.y = 43;
if (layer.length == 0) {
layer.push(card_name);
} else {
if (layer[layer.length-1][0] != card_kind) {
layer.push(card_name);
} else if(layer[layer.length-1][0] == card_kind) {
layer.push(card_name);
for(let i=0; i<layer.length; i++) {
card_now = cc.find("Canvas/New Sprite(Splash)/"+layer[i]);
if (layer[i][0] == 'C'){
card_now.zIndex = epoch++;
card_now.x = -280;
card_now.y = 232;
card_now.width = 100;
card_now.height = 150;
oc++;
cc.find("Canvas/New Sprite(Splash)/oc_label").getComponent(cc.Label).string=String(oc);
} else if(layer[i][0] == 'D') {
card_now.zIndex = epoch++;
card_now.x = -81;
card_now.y = 232;
card_now.width = 100;
card_now.height = 150;
od++;
cc.find("Canvas/New Sprite(Splash)/od_label").getComponent(cc.Label).string=String(od);
} else if(layer[i][0] == 'H') {
card_now.zIndex = epoch++;
card_now.x = 119;
card_now.y = 232;
card_now.width = 100;
card_now.height = 150;
oh++;
cc.find("Canvas/New Sprite(Splash)/oh_label").getComponent(cc.Label).string=String(oh);
} else {
card_now.zIndex = epoch++;
card_now.x = 321;
card_now.y = 232;
card_now.width = 100;
card_now.height = 150;
os++;
cc.find("Canvas/New Sprite(Splash)/os_label").getComponent(cc.Label).string=String(os);
}
}
layer = [];
}
}
} else if(type == '1'){
………..
代码中的turn与turn_store分别是上次刷新的turn与这次刷新的turn,turn!=turn_store其作用是发现是否有turn的切换, 若有切换则有一方执行了动作。在确定有一方执行动作后,若turn_store ==true则说明本次为我方回合,应当停止刷新,并执行对手上局动作相对应的动画效果。
下面贴出的是联网对战共享变量的定义,相关解释已在注释中给出。
window.card_c = []; // 我方手牌区梅花花色数组
window.card_d = []; // 我方手牌区方片花色数组
window.card_h = []; // 我方手牌区红心花色数组
window.card_s = []; // 我方手牌区黑桃花色数组
window.layer = []; // 放置区扑克牌数组
window.oc = 0; // 对手梅花数量
window.od = 0; // 对手方片数量
window.oh = 0; // 对手红心数量
window.os = 0; // 对手黑桃数量
window.mc = 0; // 我方梅花数量
window.md = 0; // 我方梅花数量
window.mh = 0; // 我方梅花数量
window.ms = 0; // 我方梅花数量
window.sch = false; // 只开一次刷新
window.unsch = false; // 保留变量
window.unschopen = false; // 若第一次为我方回合则不刷新
window.firstsch = true; // 第一次刷新
window.begin = ""; // 储存请求返回的回合信息
window.turn = false; // 上一次刷新是哪方回合
window.epoch = 1; // 用于区别节点显示的层级
window.openrobot = false; // 是否开启托管
window.value = 0; // 定位花色的数量
window.kind = ''; // 定位的花色
2.1.5 性能分析与改进
本小游戏使用Cocos Creator开发软件制作,可以发布在Web端、安卓端、微信小游戏、华为快游戏、字节跳动小游戏等平台。但由于游戏图片过多,无法发布在微信小游戏平台,且容易出现用移动端游玩时卡顿现象的发生。
本游戏采用轮转刷新制,即当对手回合时不断向服务器发送请求,当获取到对手操作时停止刷新。这就要求网络具有较高的带宽,否则将会出现卡顿或意想不到的Bug,但由于校园网网速有限,所以有时可能会出现卡顿的现象。
2.1.6 描述你改进的思路
- 对于资源包过大的问题,可以将不常使用的图片资源部署在互联网上,当需要使用这个资源时,通过http动态加载。这样即减少了资源包的冗余,也考虑到了网络负担即仅动态加载不常使用的资源。
- 对于网络带宽问题,建议不使用校园网(bushi),可以降低刷新频率,但若刷新频率过慢,可能会出现错收、漏收等情况,所以应该反复试验,取到最合适的刷新频率。(好像0.3s一次还可以)
2.1.7展示性能分析图和程序中消耗最大的函数
程序中消耗最大的函数就是loopcallback()刷新回调函数,因为动画展示、逻辑判断等核心功能都在里面,由于代码过长为了节省空间就不在此贴了,代码详情请见重要代码展示中的第一个代码块。
2.2贴出Github代码的签入记录,记录合理的commit信息
2.3 遇到的代码模块异常或结对困难及解决办法
2.3.1 困难描述
- 当开始最开始引入动画功能的时候,会出现把牌发到手牌区外、把牌发飞等异常现象。
- 在进行联网人机对战时,会出现没读取到对方操作的response,导致未显示对方操作动画,使得整局游戏出现Bug。
2.3.2 解决过程
- 通过log大法找了一圈Bug后发现写的逻辑根本没有问题,没办法了只能去网上看看有没有出现的相同问题的博客解答。不查不知道,一查吓一跳。这个Bug经被网友称为Cocos的大坑,好吧我踩了。最后通过热心网友的建议顺利解决了这个问题。
- 当出现这个问题的时候我是懵的,和室友联网人人对战的时候都是正常的,一开托管就出Bug。后来和室友一起分析探讨,可能是因为机器人反应速度太快了,把我方回合的请求直接冲掉,turn没来得及切换所致。找到这个问题后,采用了我方回合关刷新方法,最后一试,哇色~,成功了!
2.3.3 有何收获
当遇到问题的时候,自己想不出个所以然的时候,一定不能固步自封,要学会积极在网上查阅资料,和同学、室友讨论,没准会有意外收获哦!
2.3.4 评价你的队友
张静的优点:
靠谱!第一天晚上一起探讨的原型设计,第二天就给出了原型成稿,点100个赞!当自己代码出现bug好几天都改不出来的时候,真的焦虑的要命,幸亏有张静的鼓励!能成功完成这次结对编程张静功不可没!!
张静的缺点:
唯一的缺点就是没有缺点(不是张静逼我写的)
王业震的优点:
太强大了王业震!!!学习能力太牛了!!!(商业互吹一下,第一周的时候我们讨论了一下原型设计,就在我还在设计原型的时候他已经学完了js,当我学完js的时候他已经学完了cocoscreator。很牛一人!
王业震的缺点:
缺点就是太容易焦虑了!!🙅焦虑
2.3.5 此次结对作业的PSP和学习进度条
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 15 | 15 | 学习CocosCreator的使用方法,学习Java Script的基本语法结构 |
2 | 500+ | 500+ | 13 | 28 | 完成了对原型的设计,Java Script的语法初炼,实现了mainscene的跳转登录。 |
3 | 1000+ | 1500+ | 28 | 56 | 实现了联网对战中的人人对战功能,AI对战出现Bug,实现了本地对战 |
4 | 1700+ | 3200+ | 30 | 86 | 更改了联网对战中的Bug,实现了AI算法,实现了人机对战,完成了博客撰写。 |
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | 15 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 1000 | 1000 |
· Design Spec | · 生成设计文档 | 300 | 250 |
· Design Review | · 设计复审 | 100 | 100 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 1000 | 1500 |
· Coding | · 具体编码 | 2000 | 2500 |
· Code Review | · 代码复审 | 50 | 50 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | ||
· Test Repor | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 15 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 30 |
合计 | 4595 | 5555 |
三、心得
王业震:
结对编程作业的完成真的可以说是一波三折,每当告诉自己这个Bug修完后就万事大吉了,总会有新Bug出现。最后两个星期真的杀疯了😭,每天改Bug都改到1点多2点,基本就是崩溃边缘了,改到最后和队友商量说要不我们就放弃AI对战吧。不过正如诗中写道“行到水穷处,坐看云起时”,当你感觉自己要失败时,往往是成功的开始,现在我可以自信的说I made it!
回顾本次项目历程,学习颇丰、收获颇丰,从一开始6天速成Java Script语法、Cocos Creator使用,到Post/Get/Put请求傻傻分不清楚,到cocos动画的封装调用等等等等,这次结对作业学到了!提高了!
这里还要特别感谢张静同学的不断鼓励以及室友孜孜不倦的技术指导,如果没有他们的帮助,就不可能有这次项目的成功实现。Salute!
张静:
作业真的挺难的,在理解了规则之后就去买了一副扑克和队友激情打了半小时,一边打一边想AI的设计。对于这次作业很多东西都是从零开始,在b站连夜学习js以及cocos,最后看了好几个cocos扑克游戏设计教程,差不多对cocos入门。这次作业感受到了团队协作的力量,因为和王业震算得上是老搭档了,觉得每次自己一个人解决问题,很容易在一个地方一直卡住,思路局限在一个地方,但是有队友的话他就会给你提供另一种完全不同方向的想法,问题通常都会迎刃而解,团队协作真的很强!!