https://github.com/TungChintao/card_game
姓名 | 分工 | 博客链接 |
---|---|---|
童锦涛 | 前端界面,游戏逻辑,AI算法 | https://www.cnblogs.com/JosephTong/p/15329558.html |
姚天一 | 原型设计,素材收集,测试 | https://www.cnblogs.com/Error1711/p/15449376.html |
一、原型设计
[2.2.1]提供此次结对作业的设计说明
由于之前没做过原型设计,所以也不太了解,所以通过B站、博客等搜索后也经过一些同学推荐,比较主流的几款原型开发工具是Balsamiq Mockup、JustinMind、Axure RP、墨刀等等。最后,选择使用Axure RP9 + Adobe PhotoShop CC 2019来完成
原型开发工具:
Axure RP 9
Adobe PhotoShop CC 2019
原型展示
初始页面
加载页面
登陆页面
主页面
在线对局页面
等待加入房间页面
游戏内页面
-
发牌效果(由于使用软件录制帧率有限,原型动画显得不流畅,并非游戏内效果)
-
翻牌效果
-
判别对应花色并移入对应玩家手牌区,同时计数器增加移入的手牌数
-
托管功能
-
记牌器功能
[2.2.2]遇到的困难及解决方法:
困难:
- 没有接触过原型开发,零基础小白
- 素材太难寻找,好的素材需要收费,不收费的质量又欠佳
- 没有使用过PS等工具进行抠图以及制作组件
- 审美功底不足,缺乏创造美的能力
解决过程: - 在B站上找寻原型制作教程对应教程并自学
- 使用PS等工具自行制作素材
- 参考众多专业设计师方案,汲取灵感
收获: - 初步掌握了原型制作的方法
- 对PS等图片工具的使用有了一定心得
二、原型设计实现
[2.3.1]代码实现思路
- 网络接口的使用
通过js的XMLHttpRequest来实现短连接,比较频繁使用的是获取上步操作和执行玩家操作,由于后端给的是短连接,所以需要设置一个计时器每隔一段时间发送请求;同时为了实现复用,将初始化xhr的操作封装
InitXhr(url,method,parm=false){
let xhr = new XMLHttpRequest();
xhr.open(method,url);
xhr.setRequestHeader('Authorization', 'Bearer ' + global.selfInfo.token);
if(parm) xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
return xhr;
};
获取上步操作:1.在匹配系统创建房间后每隔一段时间发送此请求来判断是否有玩家加入该对局;2.玩家在不处于自己回合的时候每隔一段时间发送一个该请求,以此来实时获取对方操作并反映到UI界面上,同时进行回合转换;
fetchOperation(){
let url = URL.fetchOpUrl + global.selfRoomInfo + '/last';
if(this._lastMessageFlag) url = URL.fetchOpUrl + global.selfRoomInfo
let xhr = this.InitXhr(url,'GET');
xhr.send();
xhr.onreadystatechange = ()=>{
if(xhr.readyState == 4 && xhr.status == 200){
let returnData = JSON.parse(xhr.responseText);
//cc.log(returnData);
if(returnData.code == 200){
if(this._lastMessageFlag){
global.yourTurn = true;
this._dataStr = returnData.data.last;
return;
}
if(returnData.data.your_turn){
global.yourTurn = returnData.data.your_turn;
this._dataStr = returnData.data.last_code;
}
}
else if(returnData.code == 400){
this._lastMessageFlag = true;
}
}
}
};
执行玩家操作:1.当玩家点击抽牌区的卡牌时,发送此请求,此时无需设置请求参数,目的是获取玩家当前点击的卡牌信息,从后台获取之后,对前端UI卡牌进行实时渲染,以此来实现双方牌堆的同步;2.当玩家点击手牌区的卡牌时,发送此请求,此时需要设置参数,需要发送玩家当前点击的手牌区的卡牌信息给后台服务器端,使联机的另一方能获取到该操作并反应到UI界面上,实现双方动作的同步;
// 执行操作
executeOperation(type=-1,suit=-1,point=-1){
let data = null;
if(type == 0)
data = `type=${type}`;
else if(type == 1){
let tempdata = suit+point;
data = `type=${type}&card=${tempdata}`;
}
// cc.log(data);
let url = URL.executeOpUrl + global.selfRoomInfo;
let xhr = this.InitXhr(url,'PUT',true);
xhr.send(data);
xhr.onreadystatechange = () =>{
if(xhr.readyState == 4 && xhr.status == 200){
let returnData = JSON.parse(xhr.responseText);
// cc.log(returnData);
if(returnData.code === 200){
if(data == null){
//cc.log(returnData);
this._dataStr = returnData.data.last_code;
let data = this.parseData();
data.point = REVERSE_POINT_MAP[data.point];
this._gameModel.drawPoker(data.suit,data.point);
let dealPoker = this._gameModel.sendTopPoker();
this._gameModel.toSetArea(data.area,0,dealPoker, data.suit);
}
}
}
}
};
注意点:1.当某一方通过获取上步操作得到最后一张卡牌的信息并翻开后,另一方发送该请求时,会无法得到最后一张卡牌的信息,因为当前游戏已经结束,因此此时只能通过另一个api——获取对局信息接口,来获得最后一张卡牌的信息以此来渲染;2.每一张卡牌都不在本地制作好,而是在玩家点击抽牌区时通过发送请求从后台获取信息来实时进行渲染,这样才能实现双方卡牌的同步;
- 代码组织与内部实现设计(类图)
程序主要采用MVC设计模式,通过事件派发实现解耦合,通过控制层——GameCtrl管理数据层和视图层,并控制整个游戏流程,数据层关联卡牌数据,通过事件派发的通知视图层进行渲染,实现游戏的逻辑控制,即用户执行动作,视图层将用户操作反馈到数据层,数据层进行数据变更后通知视图层做出相应动画。其中游戏界面通过prefab渲染,控件的操作和脚本的绑定不依赖拖拽的方式,而是较多的通过代码实现,尽量减少编辑器与游戏的耦合。
- 说明算法的关键与关键实现部分流程图
对于用户视图层面的交互以及数据层的数据处理并没有用到什么特殊的算法,主要是模式的设计,AI用了比较简单的策略,以下给出逻辑流程图&代码,以及AI策略流程图&代码
逻辑流程图:
Event类(给出部分频繁使用的函数):用于实现自定义事件的接收和派发,MVC中的Model继承自该类,通过事件的派发、接收实现各个View和Model子类的解耦合(详细注释见github)
export default class Event{
on(callName, func, target = undefined){
if(!this.subscribes[callName]) { this.subscribes[callName] = []; }
this.subscribes[callName].push({f: func, del: false, target: target});
return ()=>{
this.off(callName, func);
}
};
emit(callName, ...args){
++this.m_emit_reference_count;
if(this.subscribes[callName]){
this.subscribes[callName].forEach((v) => {
if(v.f && !v.del) { v.f.apply(v.target, args); }
});
}
};
off(callName, func) {
if(this.subscrirbes[callName]){
if(func){
this.subscribes[callName].forEach( v => {
if(v.f === func) { v.del = true;}
});
}
else{
this.subscribes[callName].forEach( v => {
v.del = true;
});
}
}
if(this.m_emit_reference_count === 0){
this.clear();
}
}
subscribes = {};
m_emit_reference_count = 0;
}
Model基类:继承自Event类,负责存储游戏相关数据,维持数据状态,同时通知数据变更给视图或控制器,数据的处理以及业务逻辑都在该类中进行,游戏中的所有变更都是先进行数据的变更,再通知视图层进行UI界面的变更,与游戏数据相关的类都继承自该类。
import Event from "../Base/Event";
export default class Model extends Event{}
View基类:继承自cocos的cc.Component,并通过聚合调用Event基类实现事件监听和派发,所有与视图,UI相关的脚本都基于View基类
import Event from '../Base/Event'
var View = cc.Class({
extends: cc.Component,
ctor(){
this. _Event = new Event();
},
onLoad(){ this._Event = new Event(); },
on(callName, func, target = undefined){ return this._Event.on(callName, func, target); },
once (callName, func,target = undefined){ return this._Event.once(callName, func, target); },
emit(callName, ...args){ return this._Event.emit(callName, ...args); },
off(callName, func) { return this._Event.off(callName, func); },
});
Ai算法流程图:
从GameDB也就是数据层获取对比数据,用于AI制定出牌策略
DealCard(playerID){
// 出牌策略简易版
// (放置区无牌则点击抽牌区&&手牌比对方少) || 无手牌 抽牌
// 否则 != 放置区顶部花色 && 目前花色最多的 手牌
let setPokersNum = this._Model.setPokerNum();
let dealArea = null;
let dealPoker = null;
if(setPokersNum == 0){
dealArea = Area.sendArea;
dealPoker = this._Model.sendTopPoker();
}
else{
let setTopPokerSuit = this._Model.setTopPoker().suit;
let lessPokerPlayerID = this._Model.CmpCardNum();
if(lessPokerPlayerID === playerID || lessPokerPlayerID === -1 ||
this._Model.playerPokersNum[playerID] === 0 ||
this._Model.playerPokersNum[playerID] === this._Model.playerPokers[playerID][setTopPokerSuit].pokerNum){
dealPoker = this._Model.sendTopPoker();
dealArea = Area.sendArea;
}
else{
let maxNumSuit = 0;
let firstIndex = true;
for(let suit = 0;suit<4;suit++){
if(firstIndex && suit != setTopPokerSuit){
maxNumSuit = suit;
firstIndex = false;
}
else if(suit != setTopPokerSuit &&
this._Model.playerPokers[playerID][suit].pokerNum >
this._Model.playerPokers[playerID][maxNumSuit].pokerNum){
maxNumSuit = suit;
}
}
dealPoker = this._Model.playerPokers[playerID][maxNumSuit].GetTopPoker();
if(playerID === 0) dealArea = Area.player1List
else dealArea = Area.player2List;
}
}
setTimeout(()=>{this._View.UIPokerOnTouch(dealPoker,dealArea);},500);
};
- 贴出你认为重要的/有价值的代码片段,并解释
在线对战模式实时渲染
UIPoker类中的函数,用于在线对战的实时渲染
// 用于实时渲染
setPoker(suit,point){
this.pointLabel.string = `${POINT_MAP[point]}`;
this.pointLabel.node.color = (suit === 0 || suit === 2) ? this.blackTextColor:this.redTextColor;
if(point > 10) this.bigSuitSprite.spriteFrame = this.texFaces[point-11];
else this.bigSuitSprite.spriteFrame = this.bigSuits[suit];
this.smallSuitSprite.spriteFrame = this.smallSuits[suit];
},
OnlineManager中的获取上步操作函数,获得用户动作
fetchOperation(){
let url = URL.fetchOpUrl + global.selfRoomInfo + '/last';
if(this._lastMessageFlag) url = URL.fetchOpUrl + global.selfRoomInfo
let xhr = this.InitXhr(url,'GET');
xhr.send();
xhr.onreadystatechange = ()=>{
if(xhr.readyState == 4 && xhr.status == 200){
let returnData = JSON.parse(xhr.responseText);
//cc.log(returnData);
if(returnData.code == 200){
if(this._lastMessageFlag){
global.yourTurn = true;
this._dataStr = returnData.data.last;
return;
}
if(returnData.data.your_turn){
global.yourTurn = returnData.data.your_turn;
this._dataStr = returnData.data.last_code;
}
}
else if(returnData.code == 400){
this._lastMessageFlag = true;
}
}
}
};
数据层中存有卡牌数据,在线对战中,向用户点击的卡牌传递数据用于渲染UI
drawPoker(suit,point){
let len = this._sendPokers.length;
this._sendPokers[len-1].setPoker(suit,point);
}
在线对战中,若没有渲染翻开卡牌,卡牌信息是空的,展示如下:
在线对战中,渲染后翻开卡牌,卡牌实时加入卡牌消息,展示如下:
- 性能分析与改进
性能消耗最大,1.是监听获取上步操作的函数,当不处于自己回合时,设计的计时器不停的发送请求,造成了严重的耗时;2.是渲染卡牌ui和卡牌移动动画,共52张卡牌,每张都需要渲染和不断的移动节点,性能消耗较大;
- 描述你改进的思路
1.发送请求的改进思路:
增大计时器发送请求的间隔,即减少请求次数,但是这样会导致用户的点击不能即使得到反馈,这样对用户交互体验很不好,同时,若间隔设置过短,会导致每次返回的信息间隔太短,由于多线程的原因,使得数据发送冲突,会导致卡牌ui动作被打断、重复翻牌等bug。以此我通过多次的测试,找到了较为合适的发送请求的间隔时间,大约是800-900ms。
2.对于渲染卡牌改进思路:
由于已经使用了prefab,并且先制作出卡牌体仅渲染卡牌面的信息,因此已经做出了最大优化方案,无法继续优化,同时卡牌移动动画增加用户体验感无法简化,总的来说性能消耗也在可接受的范围内。
- 展示性能分析图和程序中消耗最大的函数
与我想象中的不一样,消耗最大的不是监听对方操作函数,而是卡牌ui移动动画函数
// 卡牌移动至抽牌区
toSendArea(poker, index){
let node = poker.view.node;
UIUtil.move(node,this.sendArea);
poker.view.Area = Area.sendArea;
cc.tween(node)
.delay(0.1*index)
.to(0.5, {position: cc.v2(0.25*index,0.25*index)})
.start();
}
// 卡牌移动至放置区
toSetArea(poker,index, fromArea){
let node = poker.view.node;
UIUtil.move(node, this.setArea);
poker.view.Area = Area.setArea;
if(fromArea === Area.sendArea){
cc.tween(node)
.delay(0.1)
.to(0.3,{scaleX:0})
.call(()=>{
poker.view.Refresh();
})
.to(0.3,{scaleX:1.2})
.delay(0.3)
.to(0.5, {position: cc.v2(0.25*index,0.25*index)}) // .to(0.5, {position: cc.v2(index*30,0)})
.start();
this.UpdatePokerPancel(false,true,true);
}
else{
cc.tween(node)
.delay(0.1)
.to(0.5, {position: cc.v2(0,0)})
.start();
this.RefreshPokerNum(poker.suit,fromArea);
this.UpdatePokerPancel(true,true,false);
}
}
// 移动到玩家手牌区
toPlayList(pokerGroup, indexLen, time, playerID){
for(let i = 0;i<indexLen;i++){
let poker = pokerGroup[i];
let node = poker.view.node;
if(playerID === 1){
UIUtil.move(node, this.player1List[poker.suit]);
poker.view.Area = Area.player1List;
}
else {
UIUtil.move(node,this.player2List[poker.suit]);
poker.view.Area = Area.player2List;
}
cc.tween(node)
.delay(time-0.1*i)
.to(0.5, {position: cc.v2(0,0)})
.start();
this.RefreshPokerNum(poker.suit,playerID-1);
this.UpdatePokerPancel(true,true,false);
}
},
- 展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路
控制变量的方案测试IsTopIndexPoker函数
将testIsTopIndexPoker注入到IsTopIndexPoker函数前执行,遍历Area枚举中的全部枚举值。如果得到的topPoker.suit 和topPoker.point值存在 则Area有解则正确,否则Area枚举值中存在错误。
testisTopIndexPoker(poker,playerID){
for (let tArea in Area) {
let topPoker = null;
if(tArea === Area.sendArea)
topPoker = this._sendPokers[this._sendPokers.length-1];
else topPoker = this._playerPokers[playerID][poker.suit].GetTopPoker();
if (topPoker.suit && topPoker.point) {
console.log("Area正确")
}else
{
console.log("Area错误")
}
}
}
测试initxhr函数功能
Url数据为 URL枚举中的所有url地址
操作为XmlHttpRequest中的 put get 操作。绑定error事件 执行error回调则证明出错。否则功能正常。
执行如下函数,响应error event即为错误
handleErrorEvent1(e) {
console.log(e);
console.log("URL地址错误!");
}
handleErrorEvent2(e){
console.log(e);
console.log("xhr PUT功能异常");
}
handleErrorEvent3(e){
console.log(e);
console.log("xhr GET功能异常");
}
TestInitXhr(){
// 测试InitXhr函数功能 1.url是否正确 2.返回的xhr功能是否正确
for (let key in URL) {
var tURL = URL[key];
let xhr = new XMLHttpRequest();
xhr.addEventListener('error', this.handleErrorEvent1);
xhr.open('PUT',tURL);
}
// 测试xhr PUT 功能是否正常
let url = URL.executeOpUrl + global.selfRoomInfo;
let xhr = new XMLHttpRequest();
xhr.addEventListener('error', this.handleErrorEvent2)
xhr.open('PUT',url);
xhr.setRequestHeader('Authorization', 'Bearer ' + global.selfInfo.token);
if(true) xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(data);
xhr.onreadystatechange = () =>{
if(xhr.readyState == 4 && xhr.status == 200){
let returnData = JSON.parse(xhr.responseText);
if(returnData.code === 200){
console.log("xhr PUT 功能正常");
}
}
}
// 测试xhr GET功能是否正常
let url = URL.fetchOpUrl + global.selfRoomInfo + '/last';
let xhr = new XMLHttpRequest();
xhr.addEventListener('error', this.handleErrorEvent3)
xhr.open('GET',url);
xhr.setRequestHeader('Authorization', 'Bearer ' + global.selfInfo.token);
if(true) xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(data);
xhr.onreadystatechange = () =>{
if(xhr.readyState == 4 && xhr.status == 200){
let returnData = JSON.parse(xhr.responseText);
if(returnData.code === 200){
console.log("xhr GET 功能正常");
}
}
}
}
[2.3.2]Github的代码签入记录,commit信息
[2.3.3]遇到的代码模块异常或结对困难及解决方法
- 困难描述
1.最开始设计的在线对战的逻辑是:每当玩家不处于自己的回合时,便激活计时器,每隔一段时间发送获取上步操作的请求,直到获取的response中的your_turn字段变为true,将本地设置的全局变量中的yourTurn的值设置为true,此时便可从response中取出data,若是对方点击抽牌区则渲染卡牌并执行翻开移动操作,若是对方点击手牌则直接执行移动操作,但存在的问题是,当对方翻开最后一张卡牌时,对局成为结束状态,获取上步操作的接口就不再返回操作了,此时是无法通过获取上步操作api获得对方操作信息的,此时游戏便卡在最后一张牌,无法翻开。
2.打包发布web的时候遇到了跨域问题
- 解决过程
1.虽然获取上步操作的api不再返回对方最后一步操作,但是由于此时全局变量中的yourTurn的值仍未false,计时器此时并不会关闭而是重复发送请求,同时,通过观察,可以发现此时接口会返回的response的code字段会变为400以表示对局结束,那么就在监听到400的返回值时,便将请求中的获取上步操作api更改为获取对局信息的api,该接口返回的response的data字段中的last便为最后一次执行的操作,问题得到解决。
2.通过大量查询资料和在github上找可行性方案,在github上找到了可以借鉴的思路,即把所有资源整合到一个单html文件内,然后自己用python模仿github上的方法,写了个可以复用的打包程序。
- 有何收获
1.以前一直认为api返回的status只是一个作为判断是否成功请求的标志,现在发现可以通过api返回的status还存在一些逻辑上的意义用于控制或改变代码流程;
2.在程序执行过程中可以根据需要动态改变请求api
[2.3.4]评价你的队友
童锦涛:
- 队友值得学习的地方
对分配的任务都能很配合的接受并完成,语言表达能力比较好,使得我们的沟通不存在歧义,同时能很好地接纳我提的建议,相比之下,我做工作就比较按自己的想法来。
- 队友需要改进的地方
也属于是ddl型选手了,很多任务堆积到了最后爆肝
姚天一:
- 队友值得学习的地方
妥妥的高手一枚,被带飞的感觉简直不要太爽,再次证明了做得好不如抱得好
自己需要改进的地方比较多,对于怎么脱离ddl驱动保持主动学习,如何平衡这门耗时巨大的课和别的课程之间的问题还要思考
- 队友需要改进的地方
前期没考虑好实现方法,走了些弯路
[2.3.5]结对作业的PSP和学习进度条(每周追加)
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | ||
· Analysis | · 需求分析 (包括学习新技术) | 1200 | 1500 |
· Design Spec | · 生成设计文档 | 20 | 30 |
· Design Review | · 设计复审 | 10 | 10 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 15 |
· Design | · 具体设计 | 300 | 260 |
· Coding | · 具体编码 | 1500 | 1200 |
· Code Review | · 代码复审 | 30 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 220 | 250 |
Reporting | 报告 | ||
· Test Repor | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
· 合计 | 3360 | 3375 |
学习进度条
童锦涛
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 25 | 25 | 学习JavaScript语言,Cocos游戏引擎 |
2 | 200 | 200 | 20 | 45 | 代码框架构建,UI初始模型搭建 |
3 | 1350 | 1550 | 20 | 65 | 完成UI设计,实现本地功能 |
4 | 970 | 2520 | 20 | 85 | 完成在线对战及托管功能,完善UI,添加额外如记牌器等功能,debug |
姚天一
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 8 | 8 | 学习原型制作、html5与css3的相关知识,对原型制作及前端有了初步了解 |
2 | 30 | 30 | 22 | 30 | 明确方向后停止css学习,继续学习原型相关,开始cocos和js的学习,制作完毕原型初稿 |
3 | 310 | 340 | 19 | 49 | 继续学习cocos与js,学习js测试相关,尝试编写测试模块 |
4 | 200 | 540 | 16 | 65 | 完成单元测试模块的编写,制作类图,完善原型,撰写博客 |
三、心得
童锦涛:
1.在架构设计的时候,使用了MVC模式,将所有卡牌数据都放在数据层中处理,并且每次都是先处理数据,再由数据层向视图层派发消息,但由于卡牌UI的移动是需要时间的,这就使得我在数据层设置了一些计时器以此使得卡牌移动UI能完成而不会被打断,但这又造成了数据和UI视图的不同步从而出现了读写冲突等一系列问题,让我吃了很多bug,然后只能设更多的计时器,不断的调计时器间隔达到一个平衡点,然后代码越写越乱,最后通过引进一个新的类——回合类才算是基本解决了问题,说到底还是初始架构没设计好,同时也是对设计模式的理解不够深入,将来还得加强。
2.对于前端其实我一向是不感冒的,但迫于形势,只能被迫学习前端知识,第一周爆肝学习js和cocos,不能说完全掌握,但对于这次作业也算是足够用的程度了,在写js的过程最让我痛苦和不适的莫过于没有完整的代码补全、函数补全功能,而且也没有很好的报错机制,就是最简单的我忘记声明某个变量直接使用时也没有报错提示,而且中途想写几个全局的枚举,发现js竟然没有枚举类......最后枚举还是用的typescript写的,很后悔为什么最开始没有选择用typescript写程序。
3.这次作业其实我最感兴趣的是对于AI的设计,一开始感觉这就是个运气游戏,对于AI唯一的想法策略就是每次执行完操作后进行一定程度的搜索,选择最优路径,但这种策略显然不行,因为存在未知性且可能的组合太多,性能消耗直接拉满好吧,因此放弃了这个想法。然后在上人工智能的时候学到了博弈树就想着能不能用min-max算法试一试,但一直没想到估价函数怎么取比较合理。因为还要写前端界面和游戏逻辑,想着ai先用个简单的策略,然后在写前端的时候用js写了个if else各种判断的简易版,然后在这期间又有各种事(毕竟我不是只有软工实践这一门课的ok?何况还是个1学分的课,感激)结果一直到最后,等我前端写的差不多了,发现博客还一堆要求,又还有团队选题要讨论,AI也只能摆烂了......
4.最后的最后,吐槽也吐不动了,本来我是个非常非常非常讨厌熬夜的人,对身体损伤太大了,而且第二天又没精神,恶性循环太可怕,结果还是熬了N天的夜,一句话总结:wzdwylkxszexszdg
姚天一:
- 作业难度
作业难度对我来说还是很大的,还好队友锦涛一人承担了主力工作。 - 作业感想
这次作业主要负责原型和测试相关,说实话对整个项目的推进没有出多少力,大部分时间都在学新的东西,不学感觉做不了,学的时候却又经常性的发生拖延,导致自己进度落下,反观队友同样也是从全新的东西起步最终完成了整个项目,对队友感到佩服的。
同时因为自己帮不上什么忙又有负罪感,真是非常感谢队友在这次作业中的付出。 - 对之后学习的启发
主动学习,早日脱离DDL驱动。