Open Topppy opened 1 year ago
几个要素
我们以B站为例
[哔哩哔哩直播姬下载](https://live.bilibili.com/liveHime?source=activity)
实际上,B站自己的直播端在直播游戏这个场景不太好用,亲测同设备的情况下,为满足直播+游戏性能,清晰度很低画质很烂。
在B站开启直播间后,可以在个人中心:我的直播间,看到服务器地址和推流码
在obs的直播设置中填写服务器地址和推流码
OBS 提供了捕捉
等能力,所以我们可以采取的方案可以有
下面分别说一下弹幕和游戏的part
b站开放了主播直播端的插件开发开放平台
[哔哩哔哩直播开放平台](https://open-live.bilibili.com/)
开发者可以
[哔哩哔哩饭贩](https://play-live.bilibili.com/)
有了密钥之后
B站给开发这提供了获取直播间数据的流程和demo代码。
import asyncio
import json
import websockets
import requests
import time
import hashlib
import hmac
import random
from hashlib import sha256
import proto
class BiliClient:
def __init__(self, roomId, key, secret, host = 'live-open.biliapi.com'):
self.roomId = roomId
self.key = key
self.secret = secret
self.host = host
pass
# 事件循环
def run(self):
loop = asyncio.get_event_loop()
websocket = loop.run_until_complete(self.connect())
tasks = [
asyncio.ensure_future(self.recvLoop(websocket)),
asyncio.ensure_future(self.heartBeat(websocket)),
]
loop.run_until_complete(asyncio.gather(*tasks))
# http的签名
def sign(self, params):
key = self.key
secret = self.secret
md5 = hashlib.md5()
md5.update(params.encode())
ts = time.time()
nonce = random.randint(1,100000)+time.time()
md5data = md5.hexdigest()
headerMap = {
"x-bili-timestamp": str(int(ts)),
"x-bili-signature-method": "HMAC-SHA256",
"x-bili-signature-nonce": str(nonce),
"x-bili-accesskeyid": key,
"x-bili-signature-version": "1.0",
"x-bili-content-md5": md5data,
}
headerList = sorted(headerMap)
headerStr = ''
for key in headerList:
headerStr = headerStr+ key+":"+str(headerMap[key])+"\n"
headerStr = headerStr.rstrip("\n")
appsecret = secret.encode()
data = headerStr.encode()
signature = hmac.new(appsecret, data, digestmod=sha256).hexdigest()
headerMap["Authorization"] = signature
headerMap["Content-Type"] = "application/json"
headerMap["Accept"] = "application/json"
return headerMap
# 获取长链信息
def websocketInfoReq(self, postUrl, params):
headerMap = self.sign(params)
r = requests.post(url=postUrl, headers=headerMap, data=params, verify=False)
data = json.loads(r.content)
print(data)
return "ws://" + data['data']['host'][0]+":"+str(data['data']['ws_port'][0])+"/sub", data['data']['auth_body']
# 长链的auth包
async def auth(self, websocket, authBody):
req = proto.Proto()
req.body = authBody
req.op = 7
await websocket.send(req.pack())
buf = await websocket.recv()
resp = proto.Proto()
resp.unpack(buf)
respBody = json.loads(resp.body)
if respBody["code"] != 0:
print("auth 失败")
else:
print("auth 成功")
# 长链的心跳包
async def heartBeat(self, websocket):
while True:
await asyncio.ensure_future(asyncio.sleep(20))
req = proto.Proto()
req.op = 2
await websocket.send(req.pack())
print("[BiliClient] send heartBeat success")
# 长链的接受循环
async def recvLoop(self, websocket):
print("[BiliClient] run recv...")
while True:
recvBuf = await websocket.recv()
resp = proto.Proto()
resp.unpack(recvBuf)
async def connect(self):
postUrl = "https://%s/v1/common/websocketInfo"%self.host
params = '{"room_id":%s}'%self.roomId
addr, authBody = self.websocketInfoReq(postUrl, params)
print(addr, authBody)
websocket = await websockets.connect(addr)
await self.auth(websocket, authBody)
return websocket
if __name__=='__main__':
try:
cli = BiliClient(
roomId = 23105976,
key = "",
secret = "",
host = "live-open.biliapi.com")
cli.run()
except Exception as e:
print("err", e)
参考这个流程那么互动弹幕的核心逻辑就是:
我们可以看一下效果
这里演示的是开源项目https://github.com/xfgryujk/blivechat的本地python服务器,这里就是实现了上述流程(mock版本)
如果我们把room ID换成B站线上正在开播的直播间ID,同样可以抓到弹幕信息。
好,弹幕我们已经搞到了,下一步,选择游戏
这里为了对比出效果,我选择了两类游戏, 实时操作类 和非实时解谜类,代表作
网页版红白机游戏的基本原理
我们以模拟器https://github.com/bfirsh/jsnes 为例
核心使用代码:
// 实例化NES模拟器
this.nes = new NES({
onFrame: this.screen.setBuffer, // canvas
onStatusUpdate: console.log,
onAudioSample: this.speakers.writeSample, // 音频
sampleRate: this.speakers.getSampleRate()
});
// 事件
this.gamepadController = new GamepadController({
onButtonDown: this.nes.buttonDown,
onButtonUp: this.nes.buttonUp
});
this.keyboardController = new KeyboardController({
onButtonDown: this.gamepadController.disableIfGamepadEnabled(
this.nes.buttonDown
),
onButtonUp: this.gamepadController.disableIfGamepadEnabled(
this.nes.buttonUp
)
});
// Load keys from localStorage (if they exist)
this.keyboardController.loadKeys();
document.addEventListener("keydown", this.keyboardController.handleKeyDown);
document.addEventListener("keyup", this.keyboardController.handleKeyUp);
document.addEventListener(
"keypress",
this.keyboardController.handleKeyPress
);
// 加载.nes:ROM
this.nes.loadROM(this.props.romData);
其Web UI
好我们目前至少跑起来了一个游戏了,下一步
一个思路:解析弹幕执行游戏指令
红白机游戏的游戏内只有6个控制键
游戏外当然还有start\pause等(暂时先不管
在js的NES 模拟器中,这些控制键被映射成为了键盘的的按键
我们要做的就是
为了方便插拔游戏,我把游戏加载在iframe中,遇到了iframe跨域问题,无法获取iframe的内容窗口并派发键盘事件,这个解决方案非常常见就是使用postMessage
在弹幕订阅页:
import KEY_MAP from '../keyboard'
/**
* 忍者神龟4等NES游戏
*/
const delay = sec => new Promise(resolve => setTimeout(resolve, sec))
export default class TurtleTrigger {
constructor() {
this.reg = /([A-Za-z0-9])/g
// iframe
this.dom = document.getElementById('iframeContain').contentWindow
this.processing = false
}
// 发送模拟键盘事件给iframe
_run = async key => {
const evtOpt = KEY_MAP[key.toUpperCase()]
this.dom.postMessage({ key: 'keydown', opt: evtOpt }, "*")
return new Promise(resolve => {
setTimeout(() => {
this.dom.postMessage({ key: 'keyup', opt: evtOpt }, "*")
resolve()
}, 100)
})
}
// 弹幕处理函数
process = async danmu => {
if (this.processing) {
console.log('trigger proccessing')
return
}
this.processing = true
// 正则把字母提取出来
const matched = danmu.match(this.reg)
console.log('matched', matched)
if (!matched) {
this.processing = false
return false
}
// console.log('run matched', matched)
// 逐一执行
for (const value of matched) {
await this._run(value)
await delay(30)
}
this.processing = false
}
}
在NES游戏页面
componentDidMount() {
window.addEventListener('message', e => {
console.log('msg=====',e.data)
const { key ,opt} = e.data
const evt = new KeyboardEvent(key, opt)
document.dispatchEvent(evt)
})
}
这个游戏遇到了一个问题,超级玛丽中,长按和短按事有不同效果的
而游戏中关卡被设计得是必须长按才能过去的,因此这里处理弹幕到时候,得实现长按效果
思路:
合并相同key,延长按压时间
// 发送模拟键盘事件给iframe
_run = async(key, duration = 1) => {
const evtOpt = KEY_MAP[key.toUpperCase()]
this.dom.postMessage({ key: 'keydown', opt: evtOpt }, "*")
return new Promise(resolve => {
setTimeout(() => {
this.dom.postMessage({ key: 'keyup', opt: evtOpt }, "*")
resolve()
// 可调节按压时长
}, duration * 100)
})
}
// 合并相同按键
sumSame(chars) {
const bucket = []
let temp = {
key: chars[0],
count: 1
}
let i = 1
while (i <= chars.length - 1) {
if (temp.key === chars[i]) {
temp.count++
} else {
bucket.push(temp)
temp = {
key: chars[i],
count: 1
}
}
i++
}
bucket.push(temp)
console.log(bucket)
return bucket
}
process = async danmu => {
// ...
if (this.mergeSameKey) {
const sum = this.sumSame(matched)
for (const value of sum) {
console.log(value)
await this._run(value.key, value.count)
await delay(30)
}
}
// ...
}
模式是类似的
不同的点在于
扫雷的操作方式:
这里如果转化为弹幕操作我们需要提取三个数据
首先设定弹幕格式为4部分,
L0 0
R0 1
那么整体的代码流程就很清晰了
弹幕订阅器
/**
* 扫雷
*/
export default class SweeperTrigger {
constructor(props) {
this.reg = /^([lLRr])([0-9]+)\s([0-9]+)/
this.dom = document.getElementById('iframeContain').contentWindow
this.processing = false
this.mergeSameKey = (props && props.mergeSameKey) || false
}
_run = async(key, x, y) => {
console.log('_run', key, x, y)
this.dom.postMessage({ key: key, opt: [x, y] }, "*")
}
process = async danmu => {
if (this.processing) {
console.log('trigger proccessing')
return
}
this.processing = true
const matched = this.reg.exec(danmu)
console.log('matched', danmu, matched)
// 非法过滤
if (!matched
|| matched.length !== 4
|| !['L', 'l', 'R', 'r'].includes(matched[1])
|| isNaN(parseInt(matched[2]))
|| isNaN(parseInt(matched[3]))) {
console.log('非法指令')
this.processing = false
return false
}
await this._run(matched[1].toUpperCase(), parseInt(matched[2]), parseInt(matched[3]))
this.processing = false
}
}
游戏页
componentDidMount() {
window.addEventListener('message', (e) => {
const { key, opt } = e.data
if (!['L', 'R'].includes(key)) return
console.log('msg=====', e.data)
// 边界检测
if (opt[0] < 0 || opt[0] > this.props.rowNum || opt[1] < 0 || opt[1] > this.props.rowNum) {
return
}
// 左键右键
if (key === 'L') {
this.handleSquareClick(opt[1], opt[0])
} else {
this.handleSquareContextMenu(opt[1], opt[0])
}
})
}
web的互动游戏可以分为三层结构
未来发展中,可以探索的几个方向
简介
弹幕互动游戏 是近年来在游戏(误:直播)行业中越来越受到欢迎的游戏形式。这种游戏通过收集玩家的弹幕信息,将其实时显示在游戏画面中,增加了互动性和趣味性,在抖音、B站等直播平台,目前已经有很多高人气的弹幕互动类游戏。其中既有第三方开发的也有平台自身研发的。
特点
弹幕互动游戏最大的特点就是弹幕互动。传统的游戏模式往往是单向的,玩家只是被动地接受游戏的内容。而弹幕互动游戏则不同,玩家可以在游戏中发射弹幕,通过与其他玩家互动,增加了游戏的趣味性和互动性。此外,弹幕互动游戏还具有以下特点:多样化的游戏模式、实时互动的体验、全球玩家的互动等。
游戏模式多样化:
https://www.bilibili.com/video/BV1xQ4y1Q7CU/?vd_source=13a87a9b97c2b7b5b32c8f91714ede90
实时又不“实时”
传统游戏直播模式,
以玩家作为信息的接收方为主,部分主播会制定自己的私人规则,来提升玩家的参与度,比如:
互动弹幕游戏模式
虽然弹幕互动游戏声称自己是实时的,但是直播弹幕互动实际上是高延迟的一个操作。具体体现在几个阶段:
用户完成一次弹幕交互,至少需要3次通信,而且是远远滞后的。
这就限制了弹幕互动游戏的种类,高实时操作性的游戏,在弹幕互动场景下变成了hard模式,这个一会我们可以体验一下。