使用案例
下面是一个使用unity-cloudrendering-js-sdk接入Unity云渲染的完整示例。
全局配置信息
sts
: Unity Cloud Rendering使用STS进行权限认证,只有配置STS之后,JS SDK才能正常工作,STS获取方式见Unity Cloud Rendering STS。 STS Token为隐私敏感信息,为防止泄露,推荐使用环境变量或者配置文件方式存储,然后在页面加载时配置STS以便SDK正常工作serverUrl
:云服务器地址,当前填写https://cloudrendering.unity.cn
userId
:用户Id,可填空email
:用户邮箱地址,可填空
const sts = process.env.sts
const serverUrl = process.env.sts
const userId = process.env.userId
const email = process.env.email
GamePlayerConfig.setConfig(serverUrl, userId, email, sts)
数据结构定义
主要包含游戏播放器状态GamePlayerStatus与客户端状态AppState。
import React, {RefObject} from 'react'
import './App.css'
import { GamePlayer, ClientInfo, GameStatusEvent, NetQualityEvent, GamePlayerConfig, GameSessionStatus, ErrorCode, GameQuality, Framerate, , KeyboardType, KeyboardLayout } from 'unity-cloudrendering-js-sdk'
import { Cookies } from 'react-cookie'
interface AppProps{}
// 游戏播放器状态
enum GamePlayerStatus {
READY,
LOADING,
PLAYING,
STOPPED,
QUEUING,
RECONNECTING
}
// 游戏播放器状态对应文字描述
const GameStatusText = [
"准备完成",
"加载中...",
"游戏中...",
"已停止",
"分配中",
"重连中..."
]
interface AppState{
// 游戏启动参数
clientInfo: ClientInfo
// 用于插入播放器的HTMLDivElement对象
playerRef: RefObject<HTMLDivElement>
// 播放按钮
playerButtonRef: RefObject<HTMLDivElement>
// 游戏播放器
player: GamePlayer | null
// 游戏播放器状态
gameStatus: GamePlayerStatus
// 游戏排队序号
rank: number
// 游戏下载进度
downloadProgress: number
// 游戏解压进度
extractProgress: number
// 延迟数据
delayData: Array<any>
avgDelay: {
totalLatency: number,
totalRes: number,
count: number,
inputLatency: number,
resLatency: number
},
// webrtc状态
rtcStatus: {
latency: number
packetLostRate: number
qp: number
}
cookies: Cookies,
// 游戏会话ID
sessionId: string
}
客户端初始化
客户端状态
游戏启动参数初始化、网页对象创建与监测数据清零
class App extends React.Component<AppProps, AppState>{
state: AppState = {
appId: '650bff584de4770035c9xxxx',
frameRate: Framerate.FRAMERATE_60,
videoAvgBps: 100,
resolution: {
width: 1920,
height: 1080,
},
rcSessionId: '',
isReconnect: false,
multipleIdentities: '',
gameQuality: GameQuality.HIGH_QUALITY,
keyboardType: KeyboardType.KEYBOARD_VIRTUAL,
keyboardLayout: KeyboardLayout.ChineseLayout,
},
// 创建HTMLDivElement对象
playerRef: React.createRef<HTMLDivElement>(),
// 创建Button
playerButtonRef: React.createRef<HTMLDivElement>(),
player: null,
gameStatus: GamePlayerStatus.READY,
rank: -1,
downloadProgress: 0,
extractProgress: 0,
// 监测数据初始化
delayData: [],
avgDelay: {
totalLatency: 0,
totalRes: 0,
count: 0,
inputLatency: 0,
resLatency: 0
},
rtcStatus: {
latency: 0,
packetLostRate: 0,
qp: 0
},
cookies: new Cookies(),
sessionId: ''
}
游戏启动
- 将启动参数填充到clientInfo中
- 初始化GamePlayer
- 调用lauchGame启动游戏
launchGame = () => {
const { clientInfo, playerRef, cookies } = this.state
// 填写APPID,获取APPID方式见操作指南
const appId = "xxxxxxxxxxxxxxxx"
if(appId){
clientInfo.appId = appId
}
// 游戏启动参数
const launchParams = "-key1 value1 -key2 value2"
if(launchParams){
clientInfo.launchParams = launchParams
}
// 如果需要用户断开连接之后一段时间内重连,需要从Cookie获取上次游戏会话ID设置重连参数
const sessionId = cookies.get(clientInfo.appId)
if(sessionId){
clientInfo.rcSessionId = sessionId
clientInfo.isReconnect = true
}
// 设置分辨率(取当前画面宽度和高度)
clientInfo.resolution = {
width: Math.max(window.innerWidth, window.innerHeight),
height: Math.min(window.innerWidth, window.innerHeight)
}
// 用户自定义鉴权token
const customerAuthToken = 'xxxx'
if(customerAuthToken){
clientInfo.customerAuthToken = customerAuthToken
}
// 设置播放器朝向(横向'landscape'/竖向'portrait')
const orientation = 'portrait'
if(orientation){
clientInfo.gameOrientation = orientation
if(orientation === 'portrait') {
clientInfo.resolution = {
width: Math.min(window.innerWidth, window.innerHeight),
height: Math.max(window.innerWidth, window.innerHeight)
}
}
}
// 启动游戏播放器
if (playerRef.current) {
this.setStyle()
const player = new GamePlayer(playerRef.current, clientInfo)
// 添加游戏状态监听器和网络状态监听器,具体监听回调示例见下方 游戏状态回调
this.addGameStatusListener()
this.addNetQualityListener()
// 启动游戏
player.launchGame()
// 注册游戏消息回调函数,可选,详情见高级功能通信部分
player.onStringMessageReceived('test', this.onReceiveStringMsg)
// 注册截图回调函数,可选,详情见高级功能截图部分
player.onCaptureSuccess(this.onCaptureSuccessCallback)
// 无交互
player.onInteractionTimeout(10, this.onInteractionTimeout)
this.setState({ player, gameStatus: GamePlayerStatus.LOADING })
}
}
运行时修改游戏分辨率或者修改游戏画面质量
运行时修改游戏分辨率
// 需要在游戏启动之后调用才能生效,设置之后立刻生效(请注意,此修改会真正修改运行的Unity游戏的分辨率,请自行在游戏端处理UI缩放和分辨率适配等问题)
player?.changeResolution(1920, 1080)
运行时修改游戏画面质量
// 需要在游戏启动之后调用才能生效,设置之后立刻生效,画面有低中高三档,可以使用sdk自带的enum GameQuality,也可以直接使用数字0、1、2分别表示低中高画质
const gameQuality = GameQuality.HIGH_QUALITY
// const gameQuality = 2
player?.changeGameQuality(gameQuality)
超时回调
可以注册无输入超时回调,可以设置超时分钟数N(2-15分钟),如果超出范围会取边界值
// 可以放在launchGame函数中
player.onInteractionTimeout(10, this.onInteractionTimeout)
回调一共会被触发两次:
- N-1分钟时触发,回调参数为(N-1,false),并不会关闭游戏
- N分钟时触发,回调参数为(N,true),同时关闭游戏。
如果未注册超时回调,无输入的情况下不会关闭游戏。
onInteractionTimeout(minutes: number, willQuit: boolean){
console.log(`${minutes} minutes no input, game will quit ${willQuit}`)
}
游戏状态回调
下方监听函数初始化均在函数addGameStatusListener中
addGameStatusListener = () => {
// 下方一系列监听注册代码
// ...
}
- 游戏开始
document.addEventListener(GameStatusEvent.OnStartGamePlayer, (event) => {
if(event instanceof CustomEvent){
console.log('start game.');
}
});
- 游戏分配信息发生变化
该回调会随游戏分配状态实时更新,其中rank值越小表示在排队越靠前,而progress值表示下载或解压进度,两者均可用于更新loading进度。
document.addEventListener(GameStatusEvent.OnGameSessionStatusChanged, (event) => {
if(event instanceof CustomEvent){
console.log("OnGameSessionStatusChanged " + JSON.stringify(event.detail))
if(event.detail.status === GameSessionStatus.ASSIGNED){
this.setState({ gameStatus: GamePlayerStatus.LOADING, rank: -1 })
} else if(event.detail.status === GameSessionStatus.CREATED){
this.setState({ gameStatus: GamePlayerStatus.LOADING, rank: -1 })
} else if(event.detail.status === GameSessionStatus.QUEUING){
this.setState({ gameStatus: GamePlayerStatus.QUEUING, rank: event.detail.rank })
} else if(event.detail.status === GameSessionStatus.DOWNLOADING){
this.setState({ gameStatus: GamePlayerStatus.LOADING, downloadProgress: event.detail.progress })
} else if(event.detail.status === GameSessionStatus.EXTRACTING){
this.setState({ gameStatus: GamePlayerStatus.LOADING, extractProgress: event.detail.progress })
} else if(event.detail.status === GameSessionStatus.INSTALL_FINISHED){
this.setState({ gameStatus: GamePlayerStatus.READY })
}
}
})
可以将appId与sessionId存储在cookie中实现游戏重连。
document.addEventListener(GameStatusEvent.OnGetSession, (event) => {
if(event instanceof CustomEvent){
const { sessionId } = event.detail
const { cookies, clientInfo } = this.state
const expired = new Date().getTime() + 24 * 60 * 60 * 1000
cookies.set(clientInfo.appId, sessionId, { path: '/', expires: new Date(expired)})
this.setState({ sessionId })
console.log("OnGameSession" + sessionId)
}
})
可以与启动按钮进行绑定,对游戏状态进行更新。
document.addEventListener(GameStatusEvent.OnStartGameSuccess, (event) => {
console.log("OnStartGameSuccess")
this.setState({ gameStatus: GamePlayerStatus.PLAYING })
const { playerButtonRef } = this.state
playerButtonRef.current?.setAttribute('style', 'display: none')
})
可以与启动按钮进行绑定,对游戏状态进行更新;
回调返回错误码与错误信息,可用于debug。
document.addEventListener(GameStatusEvent.OnStartGameFailed, (event) => {
if(event instanceof CustomEvent){
console.log("OnStartGameFailed " + JSON.stringify(event.detail))
const { code, error } = event.detail
if(code === ErrorCode.APPID_NOT_FOUND) {
console.log("appid not found", error)
} else if(code === ErrorCode.PARAMS_ERROR) {
console.log("params error", error)
} else if(code === ErrorCode.AUTH_ERROR) {
console.log("auth error", error)
} else if(code === ErrorCode.NETWORK_ERROR) {
console.log("network error", error)
} else if(code === ErrorCode.BROWSER_NOT_SUPPORT) {
console.log("browser not support", error)
} else if(code === ErrorCode.RTC_ERROR) {
console.log("rtc error", error)
} else if(code === ErrorCode.RESOURCE_ERROR) {
console.log("resource error", error)
}
const { playerButtonRef } = this.state
playerButtonRef.current?.setAttribute('style', 'display: none')
}
})
document.addEventListener(GameStatusEvent.OnGameStopped, (event) => {
if(event instanceof CustomEvent){
console.log("OnGameStopped " + JSON.stringify(event.detail))
}
})
该回调可不进行注册,按需使用。
document.addEventListener(GameStatusEvent.OnGameInvisible, (event) => {
console.log("game invisible")
})
该回调可不进行注册,按需使用。
document.addEventListener(GameStatusEvent.OnGameReVisible, (event) => {
console.log("game re visible")
this.setState({ gameStatus: GamePlayerStatus.RECONNECTING })
const { playerButtonRef } = this.state
playerButtonRef.current?.setAttribute('style', 'display: flex')
})
网络状态回调
下方监听函数初始化均在函数addNetQualityListener中
addNetQualityListener = () => {
// 下方一系列监听注册代码
// ...
}
document.addEventListener(NetQualityEvent.OnLatencyUpdate, (event) => {
if(event instanceof CustomEvent){
console.log('onLatencyUpdate' + JSON.stringify(event.detail));
const { latency } = event.detail
const { rtcStatus } = this.state
rtcStatus.latency = latency
this.setState({ rtcStatus })
}
});
document.addEventListener(NetQualityEvent.OnQpUpdate, (event) => {
if(event instanceof CustomEvent){
console.log('onLatencyUpdate' + JSON.stringify(event.detail));
const { qp } = event.detail
const { rtcStatus } = this.state
rtcStatus.qp = qp
this.setState({ rtcStatus })
}
});
高级功能
开启高级功能需要引入unity-cloudrendering-package依赖,详细参考高级功能示例。
这里对客户端与游戏消息传输进行示例。
消息传输
可以在客户端与游戏之间进行消息传输,主要包含接收消息与发送消息两个函数。
该功能允许客户端对游戏内部分辨率、游玩状态等进行设置。
// 向Unity发送消息
sendStringMessage = () => {
const { player } = this.state
if(player){
player.sendStringMessage('test', JSON.stringify(JSON.stringify({
player_name: 'xxx',
brand_id: "xxx"
})))
}
}
// 游戏创建成功之后注册回调函数
player.onStringMessageReceived('test', this.onReceiveStringMsg)
// 收到Unity消息处理逻辑
onReceiveStringMsg = (msg: string) => {
console.log(msg)
}
其他功能
// 结束游戏(如果传递了sessionId,直接关闭游戏,无法重连)
stopGame = () => {
const { player, sessionId } = this.state
player?.stopGame(sessionId)
}
页面渲染
render() {
const { playerRef, playerButtonRef, gameStatus, rank, delayData, avgDelay, rtcStatus } = this.state;
return (
<div>
<div className="game-area">
<div className="play-btn" ref={playerButtonRef}>
{ gameStatus === GamePlayerStatus.READY ? (
<svg onClick={this.launchGame} width="48" height="48" viewBox="0 0 48 48" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path
d="M24 44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C12.9543 4 4 12.9543 4 24C4 35.0457 12.9543 44 24 44Z"
fill="none" stroke="#fff" strokeWidth="4" strokeLinejoin="round"/>
<path d="M20 24V17.0718L26 20.5359L32 24L26 27.4641L20 30.9282V24Z" fill="none" stroke="#fff"
strokeWidth="4" strokeLinejoin="round"/>
</svg>
) : (
<div className="game-status-text">
{
gameStatus === GamePlayerStatus.PLAYING ? null :
gameStatus === GamePlayerStatus.QUEUING ? `${GameStatusText[gameStatus]} 序号:${rank}` : GameStatusText[gameStatus]
}
</div>
)
}
</div>
<div ref={playerRef}/>
</div>
</div>
)
}
}
export default App