首页 > 编程语言 >滚动弹幕出现位置算法

滚动弹幕出现位置算法

时间:2024-01-25 17:14:48浏览次数:49  
标签:滚动 -- 位置 距离 算法 屏幕 弹幕 上条

title: 滚动弹幕出现位置算法
date: 2024-01-25
categories: 编程
tags:
- 弹幕
- 算法
- C#

效果

显示大量弹幕、允许重叠、弹幕字号允许不同

image

约定

为了更好地进行讨论,我们先声明一些共识:

  1. 弹幕会从屏幕右边缘发射,并向左滚动

  2. 弹幕出现位置应该尽量靠上

  3. 几条弹幕之间应该尽量不要重叠,如果要重叠也要尽量重叠长度少一些

此外本文会创造/使用一些概念:

  1. 弹幕:计算的对象实体,有以下成员:

    • 发射时间:这个实际上决定了弹幕的x坐标
    • 坐标:只有y坐标,是算法最后计算出应该出现的位置
    • 宽度:根据弹幕内容计算出的宽度
    • 高度:由弹幕的字号决定
  2. 屏幕右边缘:由于弹幕是从右边出现的,所以右边缘和屏幕宽度都很重要

  3. 屏幕宽度:由窗口大小决定

  4. 位置(room),可以放置弹幕的空位,由于只需要关注屏幕右边缘线上的空位,所以位置实际上是一个一维变量,并且屏幕边缘上所有的位置合起来是一个一维数组,有以下成员:

    • 高度:位置的高度
    • 坐标:位置的坐标,实际上不是一个字段,而是由前面所有的位置高度综合算出的
    • 上条弹幕:这个位置最近发射的弹幕
  5. 停留时间:弹幕在屏幕上停留的时间

流程

如图中的弹幕情况。红色新弹幕发射时,应该插在第几行呢?

--- displayMode: compact --- gantt dateFormat ss axisFormat | 弹幕1 : 00, 4s 弹幕2 : 01, 5s 弹幕3 : 02, 4s 弹幕4 : 02, 2s 新弹幕 : crit, 05, 3s 屏幕右边缘 : milestone, 05, 0

大家肯定可以一眼看出来是第一行发射,那如何编程实现?我们先梳理一遍流程:

  1. 将弹幕按照发射时间排序,然后依次判断弹幕:

  2. 从上往下依次判断位置,如果有一个空位距离为正数,则将弹幕插入。

  3. 计算该位置中上一条弹幕距离本弹幕的距离
    (如果弹幕在边缘左侧,则为正数,在右侧为负数,负数意味着:此时在此处发射弹幕会和上一条弹幕重叠,正数则不会重叠)

  4. 如果有正数距离,则插入在这个位置。

  5. 如果没有正数距离,而且允许弹幕重叠,则选择最大的距离插入。

sort 弹幕 by 弹幕.发射时间
sort 位置(从上至下)
foreach 弹幕
    var 最大距离
    foreach 位置
        var 距离 := get_dictance(弹幕, 位置.上条弹幕)
        距离.对应位置 = 位置
        if 距离 > 0
            位置.上条弹幕 := 弹幕
            弹幕.坐标 = 位置.坐标
            break
        else
            最大距离 := max(最大距离, 距离)
    if 弹幕.坐标 = null 
        if 允许重叠
            最大距离.对应位置.上条弹幕 := 弹幕
            弹幕.坐标 = 位置.坐标
        else
            // 这条弹幕不会显示
flowchart TD start-->A-->B-->C-->D-->E--弹幕和位置遍历结束-->fin E--位置遍历结束-->H-->B E--负数-->G-->C E--正数-->F-->B start([开始]) A[将弹幕按照出现时间排序] B[依次遍历弹幕] C[依次遍历位置] D[计算该位置中上一条弹幕距离本弹幕的距离] E{位置的距离} F[插入弹幕到该位置] G[记录下目前最大的距离和相应位置] H[插入到最大距离的位置] fin([结束])

距离计算

距离表面上就是弹幕的右端距离屏幕右边缘的距离,但实际上计算时还是要考虑蛮多因素的:

如果设置一条弹幕在屏幕上停留的时间为duration秒的话,弹幕的结束时间为:

var 结束时间 := 弹幕.发射时间 + duration

而且滚动弹幕实际上是要在duration秒内,走过屏幕宽度+自身宽度的距离。我们可以算出某时刻弹幕左边缘和屏幕右边缘的距离:

func get_position (弹幕, 屏幕宽度, 某时刻, duration)
    var 弹幕已发射时间 := 某时刻 - 弹幕.发射时间
    var 弹幕要走的总长度 := 屏幕宽度 + 弹幕.宽度
    var 弹幕已走的长度 := 弹幕要走的总长度 * 弹幕已发射时间 / duration
    return 弹幕已走的长度

但是也由于这个原因,长弹幕走的速度会比短弹幕快。也就是说如果本弹幕在这个位置发射:

  • 如果上一条弹幕比本弹幕长(即速度比本弹幕快),那么本弹幕刚发射的时间就是两条弹幕距离最近的时候。

  • 如果上一条弹幕比本弹幕短(即速度比本弹幕慢),那么上条弹幕的结束时间就是两条弹幕距离最近的时候。

综上,我们可以写出函数计算弹幕的位置:

func get_dictance (弹幕, 上条弹幕)
    var 某时刻
    if 弹幕.宽度 > 上条弹幕.宽度
        某时刻 := 弹幕.发射时间
    else
        某时刻 := 弹幕.发射时间 + duration
    var 屏幕宽度 := get_viewport_width()
    var duration := get_duration()
    var 上条弹幕位置 := get_position(上条弹幕, 屏幕宽度, 某时刻, duration)
    var 本弹幕位置 := get_position(弹幕, 屏幕宽度, 某时刻, duration)
    return 上条弹幕位置 - 本弹幕位置 - 上条弹幕.宽度

处理不同大小的弹幕

但是不一定所有弹幕都是一样大小的,那“位置”的高度都不相同如何解决?如果只有大中小几种,我们也许可以按最大公约数设置高度等方法解决。但我这里要给出一种方法同时兼容所有大小的弹幕:

首先使用链表实现,使用链表是因为我们遍历位置时,更常会访问相邻的位置(如前一个位置、后一个位置)而非随机访问。

链表的每个节点都记录了当前位置的高度(位置的坐标可以由之前节点高度推算出),和在该位置中上一个弹幕的信息。

  • 当有小弹幕进入大位置时,可以把位置拆为两个相同的位置,其中靠上的位置放置新弹幕,下面的位置维持原样;

    image

  • 当有大弹幕进入小位置时,可以把相邻的几个位置合并为一个,位置的上条弹幕取时间最近一条作为新位置的上条弹幕,然后再像上一条一样拆为两个处理。

    image

我们只需要将开始时的位置,初始化为一个节点的链表,这个节点的高度是屏幕的高度。

在对一条弹幕计算的最后,在弹幕中记录下当前位置的坐标即可。

代码示例(C#)

我使用C#实现过一个软件,可供大家参考,如果还有不理解的欢迎大家联系我:

DamakuPlayer: https://github.com/Poker-sang/DanmakuPlayer/blob/master/DanmakuPlayer/Models/DanmakuUtilities.cs

标签:滚动,--,位置,距离,算法,屏幕,弹幕,上条
From: https://www.cnblogs.com/pokersang/p/17987543

相关文章

  • 数据结构与算法 pdf下载
    《数据结构与算法》涉及计算机中数据的组织、重组、移动、使用和提取等操作方法,及相关的数学分析。《数据结构与算法》所选的主题基于以下几个朴素的原则。第一,本书只讲解实用的技术,而忽略一些理论上非常虽然出色、但不太实用的算法。第二,本书既包含经典的方法,也包括最近发现的......
  • elementUI表格滚动条样式修改,隐藏表格右边留白
    修改滚动条样式//设置滚动条的宽度.el-table__body-wrapper::-webkit-scrollbar{width:4px;}//设置滚动条的背景色和圆角.el-table__body-wrapper::-webkit-scrollbar-thumb{background-color:#535353;-webkit-box-shadow:inset005pxrgba(0,0,0,0.2......
  • Unity3D 游戏中的自动寻路有怎样的算法详解
    前言Unity3D是一款非常流行的游戏引擎,它的自动寻路功能可以使游戏角色在场景中自动找到最短路径并前往目标位置。本文将详细介绍Unity3D中自动寻路的算法原理以及代码实现。对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀在游戏开发中,自动寻路是......
  • 欧几里得算法
    欧几里得算法用于求解两个数\(a,b\)的最大公约数,\(\gcd(a,b)=\gcd(b,a\bmodb)\),为了方便证明,我们约定\(a>b\),证明:设\(r=a\bmodb=a-k\cdotb\),\(d\mida\)且\(d\midb\),显然\(a=k\cdotb+r\),那么\(\frac{r}{d}=\frac{a}{d}-\frac{k\cdot......
  • java中双指针算法(快指针与慢指针)
    双指针法:设置两个指针,分别是快指针和慢指针,分别是i和j。  设置一个变量temp用来储存第一个数据nums[0] 过程:1.nums[0]  temp  他们两个相等  那么temp不需要改变,i=i+1,j变,i=1,j=1   2.nums[i=1],temp  他们两个相等,temp不变,i=i+1,j不......
  • 文心一言 VS 讯飞星火 VS chatgpt (186)-- 算法导论14.1 3题
    三、用go语言,写出OS-SELECT的非递归版本。文心一言:下面是使用Go语言编写的OS-SELECT的非递归版本:funcOS_SELECT(T*Node,kint)*Node{//找到最小秩的节点minRankNode:=findMinRankNode(T)//如果最小秩的节点就是目标节点,则返回该节点if......
  • Unity3D Rts游戏里的群体移动算法是如何实现的详解
    实时战略(RTS)游戏是一种以管理和控制虚拟军队为主题的游戏类型。在这类游戏中,玩家需要控制大量的单位进行战斗、资源采集和建设等操作。其中,群体移动算法是实现这些操作的关键之一。本文将详细介绍Unity3DRTS游戏中群体移动算法的实现原理和代码实现。对啦!这里有个游戏开发交流小......
  • Tarjan 算法(超详细!!)
    Tarjan算法前言说来惭愧,这个模板仅是绿的算法至今我才学会。我还记得去年CSP2023坐大巴路上拿着书背Tarjan的模板。虽然那年没有考连通分量类似的题目。现在做题遇到了Tarjan,那么,重学,开写!另,要想学好此算法的第一件事——膜拜Tarjan爷爷。Tarjan算法到底是什么其......
  • 2024/1/24 算法笔记
    1.快速幂模板虽然前面可能写过了,但是遇到了就再贴一下。LLqmi(LLa,LLk,LLp){LLres=1%p;while(k){if(k&1)res=res*a%p;a=a*a%p;k=k>>1;}returnres;}2.最大子段和给一个数组,求其中元素总和最大......
  • PPO算法——PPOxFamily
    1.决策智能目的就是搜索最优解,方法主要有两种:从模仿中学习、从试错中学习从模仿中学习通过棋谱来学棋优势:简洁直观劣势:数据要求高,可迁移性差从试错中学习通过对弈来学习优势:可以不断提升和强化劣势:过程复杂,效率和稳定性有待提高深度强化学习——更强大、更通用、更稳定......