We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本文介绍https://github.com/flutte-danmaku/flutter_danmaku该项目的设计思路和一些实现细节。
简历筛选大大看过来 -> 简历相关扩展
技术细节:
下文将介绍各个模块的实现细节和思路
子弹的设计要考虑碰撞检测,宽度对速度的影响,变速需求,个性化子弹的需求,偏移量,静止弹幕以及滚动弹幕的需求。
首先简单介绍子弹模型中重要的成员变量
class FlutterDanmakuBulletModel { UniqueKey id; // 通过id查询该子弹 UniqueKey trackId; // 绑定的轨道id UniqueKey prevBulletId; // 轨道中上一个子弹的id ... double _runDistance = 0; // 已经移动的距离 double everyFrameRunDistance; // 每帧移动的距离 ... /// 滚动子弹的x轴位置 double get offsetX => _runDistance - bulletSize.width; /// 子弹最大可跑距离 子弹宽度+墙宽度 double get maxRunDistance => bulletSize.width + FlutterDanmakuConfig.areaSize.width; /// 子弹整体脱离右边墙壁 bool get allOutRight => _runDistance > bulletSize.width; /// 子弹整体离开屏幕 bool get allOutLeave => _runDistance > maxRunDistance; /// 子弹当前执行的距离 double get runDistance => _runDistance; /// 剩余离开的距离 double get remanderDistance => needRunDistace - runDistance; /// 直到消失需要移动的距离 double get needRunDistace => FlutterDanmakuConfig.areaSize.width + bulletSize.width; /// 离开屏幕剩余需要的时间 double get leaveScreenRemainderTime => remanderDistance / everyFrameRunDistance; /// 子弹执行下一帧 void runNextFrame() { _runDistance += everyFrameRunDistance * FlutterDanmakuConfig.bulletRate; } }
从成员变量来看 会发现子弹的距离判断都需要依赖子弹本身的宽度和弹幕墙的宽度,这是由于子弹的出现从头部计算而子弹的隐藏和碰撞从尾部计算。
保存所有弹幕的方式,思考过两种方案。 一种是在轨道上通过数组保存该轨道上所有的子弹Id。使用Map作为索引Key为Id,Value为下标。 一种是以链表的形式链接轨道上的所有子弹。使用Map作为索引Key为Id,Value为Model。
最终选择以链表的形式保存弹幕的方案。 主要出于以下考虑:
链表 + Map类似LRUCache算法,不过LRUCache算法使用的是双向链表 + HashMap LRUCache使用HashMap实现O(1)的查找,使用双向链表实现O(1)的节点删除和移动。
链表删除节点 [https://leetcode-cn.com/problems/shan-chu-lian-biao-de-jie-dian-lcof/] LRUCache [https://leetcode-cn.com/problems/lru-cache/]
每个子弹通过prevBulletId以链表的数据结构联接,使用Map作为索引。
class FlutterDanmakuBulletManager { Map<UniqueKey, FlutterDanmakuBulletModel> _bullets = {}; Map<UniqueKey, FlutterDanmakuBulletModel> get bulletsMap => _bullets; ... }
插入弹幕的时候,需要遍历每个轨道是否允许插入新的弹幕,除了考虑该轨道最后一个子弹的一些特征外,还需要判断新插入的轨道是否会追尾。
由于弹幕越长,跑的越快(基本速率 + (宽度 / 倍率))的特性,即便前一个弹幕已经过了一半,还需要考虑新的弹幕是否会速度快到导致追尾。
// 轨道注入子弹是否会碰撞 static bool trackInsertBulletHasBump(FlutterDanmakuBulletModel trackLastBullet, Size needInsertBulletSize, {int offsetMS = 0}) { // 是否离开了右边的墙壁 if (!trackLastBullet.allOutRight) return true; double willInsertBulletEveryFramerateRunDistance = FlutterDanmakuUtils.getBulletEveryFramerateRunDistance(needInsertBulletSize.width); bool hasInsertOffsetSpace = true; double willInsertBulletRunDistance = offsetMS == null ? 0 : (offsetMS / FlutterDanmakuConfig.unitTimer) * willInsertBulletEveryFramerateRunDistance; if (offsetMS != null) hasInsertOffsetSpace = hasInsertOffsetSpaceComputed(trackLastBullet, willInsertBulletRunDistance); if (!hasInsertOffsetSpace) return true; // 要注入的节点速度比上一个快 if (willInsertBulletEveryFramerateRunDistance > trackLastBullet.everyFrameRunDistance) { // 是否会追尾 // 将要注入的弹幕全部离开减去上一个弹幕宽度需要的时间 double willInsertBulletLeaveScreenRemainderTime = remainderTimeLeaveScreen(willInsertBulletRunDistance, 0, willInsertBulletEveryFramerateRunDistance); return trackLastBullet.leaveScreenRemainderTime > willInsertBulletLeaveScreenRemainderTime; } else { return false; } } // 子弹剩余多少帧离开屏幕 static double remainderTimeLeaveScreen(double runDistance, double textWidth, double everyFramerateDistance) { assert(runDistance >= 0); assert(textWidth >= 0); assert(everyFramerateDistance > 0); double remanderDistance = (FlutterDanmakuConfig.areaSize.width + textWidth) - runDistance; return remanderDistance / everyFramerateDistance; }
需要做的一些判断
计算子弹离开剩余帧数 = 离开屏幕剩余距离 - 每帧需要跑的距离。
首先需要计算该弹幕的尺寸,通过获取弹幕的宽度,用来调整弹幕每帧的行进距离,使得弹幕墙整体错落有致。
// 根据文字长度计算每一帧需要run多少距离 static double getBulletEveryFramerateRunDistance(double bulletWidth) { assert(bulletWidth > 0); return FlutterDanmakuConfig.baseRunDistance + (bulletWidth / FlutterDanmakuConfig.everyFramerateRunDistanceScale); }
需要查询可用的轨道,如果没有找到可用的轨道,返回一个错误信息,让业务层自行处理
if (track == null) return AddBulletResBody( AddBulletResCode.noSpace, );
如果允许插入,根据弹幕类型分别记录。
轨道使弹幕不需要计算X轴碰撞。轨道的设计需要考虑弹幕墙的高度,Y轴区间,以及满足展示弹幕区域的业务需求,并且需要提供弹幕链表的头节点以方便遍历操作。
首先根据文字配置获取文字高度,然后填充满弹幕墙。并且每一条轨道都需要偏移弹幕墙的剩余高度 / 2 使其居中。
// 补足屏幕内轨道 void buildTrackFullScreen() { Size singleTextSize = FlutterDanmakuUtils.getDanmakuBulletSizeByText('s'); while (allTrackHeight < (FlutterDanmakuConfig.areaSize.height - singleTextSize.height)) { buildTrack(singleTextSize.height); } }
根据轨道记录的最后一个子弹,通过遍历取上一颗子弹,来删除所有的弹幕。
// 删除轨道上的所有子弹 void delBullletsByTrack(FlutterDanmakuTrack track, Map<UniqueKey, FlutterDanmakuBulletModel> bulletMap) { if (track.bindFixedBulletId != null) bulletMap.remove(track.bindFixedBulletId); UniqueKey prevBulletId = track.lastBulletId; while (prevBulletId != null) { UniqueKey _prevBulletId = bulletMap[prevBulletId]?.prevBulletId; bulletMap.remove(prevBulletId); prevBulletId = _prevBulletId; } }
弹幕的播放通过控制器控制,控制器由定时器驱动,每隔一段时间,就会遍历所有的子弹,让每个子弹Model的位置数据更新到下一帧的位置数据,然后调用setState()统一渲染。
void run(Function nextFrame, Function setState) { _timer = Timer.periodic(Duration(milliseconds: FlutterDanmakuConfig.unitTimer), (Timer timer) { // 暂停不执行 if (!FlutterDanmakuConfig.pause) { // 将所有子弹的位置参数更新为下一帧 nextFrame(); // 最后统一渲染 setState(() {}); } }); } /// 子弹执行下一帧 void runNextFrame() { _runDistance += everyFrameRunDistance * FlutterDanmakuConfig.bulletRate; }
定时器并不会到点就执行,而是到点插入事件队列,由于Dart与JS使用相同的事件循环模型,那么定时器会被插入宏任务队列。 需要注意如果前面的执行任务耗费了过多的时间,那么会严重影响下次调用setState的时机导致掉帧。
在弹幕组件中,如何规避掉帧
事件循环 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop 寻找公共父祖先 如果多次修改dom节点会导致浏览器的多次重排,那么比较好的方案是找出两个节点的公共祖先节点,从祖先节点开始替换。https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/
简单介绍一下这个项目
https://github.com/flutte-danmaku/flutter_danmaku
基于Flutter的弹幕项目
实现以下功能
欢迎点击https://a62527776a.github.io/flutter_danmaku_demo/index.html试用
The text was updated successfully, but these errors were encountered:
No branches or pull requests
简历筛选大大看过来 -> 简历相关扩展
技术细节:
模块
该项目主要围绕这三块来进行开发。基于这三个模块,解决诸如弹幕的碰撞检测,轨道的动态更新,控制器的渲染等问题。
下文将介绍各个模块的实现细节和思路
子弹
子弹的设计要考虑碰撞检测,宽度对速度的影响,变速需求,个性化子弹的需求,偏移量,静止弹幕以及滚动弹幕的需求。
子弹的基本设计
首先简单介绍子弹模型中重要的成员变量
从成员变量来看 会发现子弹的距离判断都需要依赖子弹本身的宽度和弹幕墙的宽度,这是由于子弹的出现从头部计算而子弹的隐藏和碰撞从尾部计算。
弹幕的数据结构
保存所有弹幕的方式,思考过两种方案。
一种是在轨道上通过数组保存该轨道上所有的子弹Id。使用Map作为索引Key为Id,Value为下标。
一种是以链表的形式链接轨道上的所有子弹。使用Map作为索引Key为Id,Value为Model。
最终选择以链表的形式保存弹幕的方案。
主要出于以下考虑:
链表 + Map类似LRUCache算法,不过LRUCache算法使用的是双向链表 + HashMap LRUCache使用HashMap实现O(1)的查找,使用双向链表实现O(1)的节点删除和移动。
每个子弹通过prevBulletId以链表的数据结构联接,使用Map作为索引。
弹幕的追尾
插入弹幕的时候,需要遍历每个轨道是否允许插入新的弹幕,除了考虑该轨道最后一个子弹的一些特征外,还需要判断新插入的轨道是否会追尾。
由于弹幕越长,跑的越快(基本速率 + (宽度 / 倍率))的特性,即便前一个弹幕已经过了一半,还需要考虑新的弹幕是否会速度快到导致追尾。
需要做的一些判断
插入一颗子弹
首先需要计算该弹幕的尺寸,通过获取弹幕的宽度,用来调整弹幕每帧的行进距离,使得弹幕墙整体错落有致。
需要查询可用的轨道,如果没有找到可用的轨道,返回一个错误信息,让业务层自行处理
如果允许插入,根据弹幕类型分别记录。
轨道
轨道使弹幕不需要计算X轴碰撞。轨道的设计需要考虑弹幕墙的高度,Y轴区间,以及满足展示弹幕区域的业务需求,并且需要提供弹幕链表的头节点以方便遍历操作。
初始化轨道
首先根据文字配置获取文字高度,然后填充满弹幕墙。并且每一条轨道都需要偏移弹幕墙的剩余高度 / 2 使其居中。
删除轨道上所有子弹
根据轨道记录的最后一个子弹,通过遍历取上一颗子弹,来删除所有的弹幕。
控制器
弹幕的播放通过控制器控制,控制器由定时器驱动,每隔一段时间,就会遍历所有的子弹,让每个子弹Model的位置数据更新到下一帧的位置数据,然后调用setState()统一渲染。
在弹幕组件中,如何规避掉帧
简单介绍一下这个项目
https://github.com/flutte-danmaku/flutter_danmaku
基于Flutter的弹幕项目
实现以下功能
Features
欢迎点击https://a62527776a.github.io/flutter_danmaku_demo/index.html试用
The text was updated successfully, but these errors were encountered: