一、结对探索
1.1 队伍基本信息
结对编号:9;队伍名称:这个人很懒;
学号 | 姓名 | 作业博客链接 | 具体分工 |
---|---|---|---|
012004114 | 林毅 | 后端,原型设计,视频剪辑 | |
052004130 | 潘思源 | 前端,UI,AI |
1.2 描述结对的过程
1.3 非摆拍的两人在讨论设计或结对编程过程的照片
(bushi)
二、原型设计
2.1 原型工具的选择
墨刀 简单易上手 (bushi 看大家都用这个
2.2 遇到的困难与解决办法
困难:对于一下比较精美的图片墨刀需要收费,不愿意当大冤种充钱,就要想些解决方法
解决办法:对于一些比较精美的logo就选择用自己用ipad的procreate进行绘制
2.3 原型作品链接
https://modao.cc/app/eu97UaLbrjp720huGEpDvl #逍遥骰原型-分享
2.4 原型界面图片展示
创新点1:提供在线和离线两个模式,在线模式用户可以根据自己的id来在线匹配对手,离线模式用户只可享受人机对战和本地双人对战的功能
创新点2:人机对战,笔者设计了6个难度的AI算法来适配不同的用户,只有通过前面的关卡才能解锁后面的关卡。
在线功能提供:人机对战、本地双人对战、在线匹配对手对战
离线功能提供:人机对战、本地双人对战
本地双人对战:两玩家在同一台设备上进行操作,盘面可供两人操作
在线匹配对手对战:玩家通过将自己的id发送请求,并等待响应,在超时时间范围内,若有人同样发起请求,则服务器向这两玩家做出响应,两者可在线对战。
两创新点的原型图片如下:
其他功能UI如下:
三、编程实现
3.1 网络接口的使用
一些将为初级的接口就为普通的请求响应
验证码功能需将邮箱权限放开,与服务器进行交互后返回可用的接口
在线匹配对战时,利用websocket实现即时通讯,后端监听连接的建立和数据的转发,前端利用socket的onopen、onmessage等接口进行信息交互
后端利用postman进行接口测试,具体测试接口如下:
3.2 代码组织与内部实现设计(类图)
前端:
后端:
3.3 说明算法的关键与关键实现部分流程图
其实ai每一步最多只有三种不同的选择,我通过一个双层神经网络(第一层的输入是一个19维向量)给每一种选择评分
取最高分的选择为输出结果。
3.4 贴出重要的/有价值的代码片段并解释
前端:
void updatemat(int sign, List<int> newmat, index, context) {//这是PvP版,PvE调用aimove函数,联机对战监听websocket
if (sign == 0) {
updata = newmat;
} else {
downdata = newmat;
}
check(sign, index);//检查是否有消除
downpoint = updatePoint(downdata);//更新矩阵
uppoint = updatePoint(updata);
turn == 0 ? turn = 1 : turn = 0;//切换回合
check_terminal(context);//检查游戏是否结束
print(turn);
notifyListeners();//重新渲染画面
}
//函数在widget tree 顶端工作,负责重新渲染/同步数据
void connect(String id, context) {
channel = IOWebSocketChannel.connect('ws://120.77.79.99:8085/imserver/$id');
channel.stream.listen((event) {//监听websocket数据
print(event);
if (r == 0) {
Map<String, dynamic> data = jsonDecode(event.toString());//json解码
updata = parse(data["mat2"]);//更新数据
downdata = parse(data["mat1"]);
downpoint = updatePoint(updata);//更新分数
uppoint = updatePoint(downdata);
print("\n\n\n\n\ndata received");
turn = 1;
check_terminal(context);//检查游戏结束
notifyListeners();
}
if (r == 1) r = 0;
});
}
后端:
@ServerEndpoint("/imserver/{userId}")
@Component
public class WebSocketServer {
static Log log=LogFactory.get(WebSocketServer.class);
/**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/
private static int onlineCount = 0;
/**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/
private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
private Session session;
/**接收userId*/
private String userId="";
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session,@PathParam("userId") String userId) {
this.session = session;
this.userId=userId;
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
webSocketMap.put(userId,this);
//加入set中
}else{
webSocketMap.put(userId,this);
//加入set中
addOnlineCount();
//在线数加1
}
log.info("用户连接:"+userId+",当前在线人数为:" + getOnlineCount());
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("用户:"+userId+",网络异常!!!!!!");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if(webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
//从set中删除
subOnlineCount();
}
log.info("用户退出:"+userId+",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息:"+userId+",报文:"+message);
//可以群发消息
//消息保存到数据库、redis
if(StringUtils.isNotBlank(message)){
try {
//解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
//追加发送人(防止串改)
jsonObject.put("fromUserId",this.userId);
String toUserId=jsonObject.getString("toUserId");
//传送给对应toUserId用户的websocket
if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){
webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());
}else{
log.error("请求的userId:"+toUserId+"不在该服务器上");
//否则不在这个服务器上,发送到mysql或者redis
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:"+this.userId+",原因:"+error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 发送自定义消息
* */
public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {
log.info("发送消息到:"+userId+",报文:"+message);
if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){
webSocketMap.get(userId).sendMessage(message);
}else{
log.error("用户"+userId+",不在线!");
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
3.5 性能分析与改进
前端:减少重新渲染的范围,优化widget tree 结构,每次数据更新时仅局部刷新,只在数据会被用户看到或空闲的时候重新渲染
后端:可以看出,当对每一个函数进行调用时,CPU的占用率并不大,外部调用时所占用CPU较大(关闭项目操作)。故此时的接口的性能都较优。
但小游戏应考虑若有很多玩家同时并发发起匹配请求的高并发问题(或许像羊了个羊一样火了呢(bushi)),故笔者利用redis缓存缓存玩家请求数据,并将其发送至Kafka消息队列,进行逐一处理以解决并发、大数据问题。
3.6 单元测试
@SpringBootTest
class XiaoYaoShaiApplicationTests {
@Test
void contextLoads() {
}
@Autowired
private UserMapper userMapper;
/**
* mybatis-plus测试
*/
@Test
void testInsert(){
int result = userMapper.insert(new User("228751565@qq.com","123456"));
System.out.println("result:" + result);
}
@Autowired(required = false)
private RedisUtil redisUtil;
/**
* redis缓冲测试
*/
@Test
void getName(){
redisUtil.set("2287511565@qq.com","5654");
redisUtil.expire("2287511565@qq.com",30);
System.out.println(redisUtil.get("2287511565@qq.com"));
System.out.println(redisUtil.getExpire("2287511565@qq.com"));
String sources = "0123456789"; // 加上一些字母,就可以生成pc站的验证码了
Random rand = new Random();
StringBuffer flag = new StringBuffer();
for (int j = 0; j < 6; j++)
{
flag.append(sources.charAt(rand.nextInt(9)) + "");
}
System.out.println(flag.toString());
}
}
3.7 贴出GitHub的代码签入记录,合理记录commit信息
四、总结反思
4.1 本次任务的PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 3320 | 4320 |
· Analysis | · 需求分析 (包括学习新技术) | 200 | 200 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 60 | 60 |
· Coding | · 具体编码 | 3000 | 4000 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 70 | 70 |
Reporting | 报告 | 10 | 10 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
· 合计 | 6890 | 8890 |
4.2 学习进度条(每周追加)
前端:
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 20 | 20 | 0 | 0 | 深刻反思了自己的懒惰 |
2 | 200 | 220 | 1 | 1 | 深刻反思了自己的懒惰 |
3 | 3980 | 4200 | 39 | 40 | 学到了生命的可贵;人类在茫茫的宇宙中是多么的渺小;人类的赞歌就是勇气的赞歌,人类在极度危险的环境下能激发出无限的可能! |
后端:
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 0 | 0 | 0 | 0 | 嵌入式学明白了一点 |
2 | 0 | 0 | 0 | 0 | 接口学明白了一点 |
3 | 2173 | 2173 | 30 | 30 | 对很久没用的技术重新巩固了一遍,加深了自己对其的理解。对服务器上的数据库加密更熟练了一点(被攻击太多次了),websocket连接有了更深的理解 |
4.3 最初想象中的产品形态、原型设计作品、软件开发成果三者的差距如何?
有很多情况在一开始设计的时候没有设想的,开发过程中花了很多时间fix,然后实际运行时又出现了很多开发过程中没有预想到的情况,又花了很多时间。主要是游戏逻辑,和联机同步问题比较麻烦。
一开始想法比较简单,感觉小游戏用不了多久就能搞定,于是乎前两周就没去新建文件夹。等开始筹备的时候,总是把各个操作想的太简单,中途也遇到了很多bug,很多一开始预想的功能页面再最后只能不断删减。
产品基本实现了最初设想。
4.4 评价你的队友
潘思源
队友:林毅
值得学习的地方:
- 代码易懂、简单、高效
- 逻辑清晰
- 代码高内聚,低耦合
需要改进的地方:
- 射手打得稀烂
林毅
队友:潘思源
值得学习的地方:
- 代码能力强,技术栈丰富
- 勤奋刻苦,为了赶项目4点睡8点起
- 思维开阔,能有很多idea
需要改进的地方:
- 太努力了,制造焦虑
4.5 结对编程作业心得体会
潘思源:
一定要早点开工啊,不要拖啊。
前期准备比实际写代码更重要,前后端对接一定要事先沟通,一边写一边商量很痛苦
ai一开始打算用强化学习的,后来一直拖就没去学。所以说啊,不能拖!
写代码该重构就重构,不要把时间浪费在屎山上。
测试过了不意味着实机能跑,问题只会在运行过程中出现。
林毅:
因为时间问题导致最后没能交出一份满意的项目实属可惜。
bin的名言:“一个项目的工程量要按自己设想的两倍来计算。”这次算是狠狠的领教到了
本次作业的难度在基础功能上不算难,匹配机制、即时通讯都不算难,但可发挥性很大。AI算法可以算是能够无限卷下去的方向,研究的过程会触及到之前很多领域没有涉猎过的知识,很八错,很有趣。
在后端的开发上也比较能够得心应手了,单人进行后端开发目前认为问题不大,等到后期项目比较大时,涉及到多模块多人合作的后端开发,到时可能会遇上一些较为棘手的问题。
这次可惜没有往小程序上线方向走,下次有机会想尝试一下上线一个小程序。