-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
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
直播弹幕互动游戏 #82
Comments
我们来试着整一个直播弹幕互动游戏玩一下几个要素
我们以B站为例 直播使用b站的官方直播软件:(目前不兼容非M1的mac)[哔哩哔哩直播姬下载](https://live.bilibili.com/liveHime?source=activity) 实际上,B站自己的直播端在直播游戏这个场景不太好用,亲测同设备的情况下,为满足直播+游戏性能,清晰度很低画质很烂。 OBS+第三方插件 + 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部分,
那么整体的代码流程就很清晰了 代码弹幕订阅器 /**
* 扫雷
*/
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模式,这个一会我们可以体验一下。
The text was updated successfully, but these errors were encountered: