|
|
@@ -1,58 +1,65 @@
|
|
|
# Game SDK
|
|
|
|
|
|
-平台在加载游戏前会自动向页面注入 `window.GameSDK`,游戏里直接调用即可,无需任何初始化。
|
|
|
+这个包同时提供两部分能力:
|
|
|
|
|
|
----
|
|
|
+1. 浏览器运行时的全局 `GameSDK` 类型定义。
|
|
|
+2. 发布到正式平台的 CLI 命令:`game-sdk publish`、`game-sdk asset`、`game-sdk delete`。
|
|
|
|
|
|
-## 安装
|
|
|
+目标用法很简单:
|
|
|
|
|
|
-```bash
|
|
|
-npm install git+https://gitee.com/personal-site-micro/game-sdk.git
|
|
|
-```
|
|
|
+1. 游戏项目安装这个包。
|
|
|
+2. 游戏代码里直接使用全局 `GameSDK`。
|
|
|
+3. `package.json` 里写 `npm run build` 和 `npm run publish`。
|
|
|
+4. 用 `npm run publish` 发布到正式平台。
|
|
|
|
|
|
-安装后 TypeScript 自动识别 `GameSDK` 全局类型,无需手动 import。
|
|
|
+这个 SDK 只面向正式平台发布,不支持本地部署平台服务。
|
|
|
|
|
|
----
|
|
|
+## 快速开始
|
|
|
|
|
|
-## API
|
|
|
+### 1. 安装
|
|
|
|
|
|
-```ts
|
|
|
-GameSDK.gameId // string — 当前游戏的 id
|
|
|
-GameSDK.getPlayerName() // string — 当前玩家昵称(来自平台 localStorage)
|
|
|
-GameSDK.submit(score: number) // Promise — 提交分数
|
|
|
+如果当前还没发到 npm,直接用 git 安装:
|
|
|
+
|
|
|
+```bash
|
|
|
+npm install git+https://gitee.com/personal-site-micro/game-sdk.git
|
|
|
```
|
|
|
|
|
|
-`submit` 返回值:
|
|
|
+再安装打包工具,例如:
|
|
|
|
|
|
-```ts
|
|
|
-{ ok: true; rank: number } // 成功,rank 为当前排名
|
|
|
-{ ok: false; error: string } // 失败
|
|
|
+```bash
|
|
|
+npm install --save-dev esbuild
|
|
|
```
|
|
|
|
|
|
----
|
|
|
-
|
|
|
-## 开发一个游戏
|
|
|
+### 2. 配置脚本
|
|
|
|
|
|
-### 1. 初始化项目
|
|
|
+`package.json`:
|
|
|
|
|
|
-```bash
|
|
|
-mkdir my-game && cd my-game
|
|
|
-npm init -y
|
|
|
-npm install git+https://gitee.com/personal-site-micro/game-sdk.git
|
|
|
-npm install --save-dev esbuild
|
|
|
+```json
|
|
|
+{
|
|
|
+ "scripts": {
|
|
|
+ "build": "esbuild src/game.js --bundle --outfile=bundle.js --platform=browser --format=iife",
|
|
|
+ "publish": "npm run build && game-sdk publish"
|
|
|
+ }
|
|
|
+}
|
|
|
```
|
|
|
|
|
|
-在 `package.json` 加 build 脚本:
|
|
|
+### 3. 配置发布信息
|
|
|
+
|
|
|
+在项目根目录创建 `game-sdk.config.json`:
|
|
|
|
|
|
```json
|
|
|
-"scripts": {
|
|
|
- "build": "esbuild src/game.js --bundle --outfile=bundle.js --platform=browser --format=iife"
|
|
|
+{
|
|
|
+ "game-id": "my-game",
|
|
|
+ "name": "我的游戏",
|
|
|
+ "bundle": "./bundle.js",
|
|
|
+ "description": "游戏描述",
|
|
|
+ "controls": "方向键移动"
|
|
|
}
|
|
|
```
|
|
|
|
|
|
-### 2. 编写游戏
|
|
|
+### 4. 编写游戏入口
|
|
|
|
|
|
-游戏入口为 `src/game.js`(或 `.ts`)。平台会把它注入到页面里,游戏需要自己挂载到 `#game-container`:
|
|
|
+`src/game.js`:
|
|
|
|
|
|
```js
|
|
|
;(function () {
|
|
|
@@ -63,121 +70,389 @@ npm install --save-dev esbuild
|
|
|
canvas.style.cssText = 'width:100%;height:100%;display:block;'
|
|
|
container.appendChild(canvas)
|
|
|
|
|
|
- // ... 游戏逻辑 ...
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
+ let width = 0
|
|
|
+ let height = 0
|
|
|
+ let score = 0
|
|
|
+ let submitted = false
|
|
|
+ let startedAt = 0
|
|
|
+ let ended = false
|
|
|
+
|
|
|
+ function resize() {
|
|
|
+ const rect = container.getBoundingClientRect()
|
|
|
+ width = rect.width
|
|
|
+ height = rect.height
|
|
|
+
|
|
|
+ if (width <= 0 || height <= 0) return false
|
|
|
+
|
|
|
+ const dpr = window.devicePixelRatio || 1
|
|
|
+ canvas.width = width * dpr
|
|
|
+ canvas.height = height * dpr
|
|
|
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
|
+ ctx.scale(dpr, dpr)
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ async function endGame() {
|
|
|
+ if (submitted) return
|
|
|
+ submitted = true
|
|
|
+ ended = true
|
|
|
|
|
|
- // 游戏结束时提交分数
|
|
|
- async function gameOver(score) {
|
|
|
const result = await GameSDK.submit(score)
|
|
|
if (result.ok) {
|
|
|
- console.log('第', result.rank, '名')
|
|
|
+ console.log('当前排名:', result.rank)
|
|
|
+ } else {
|
|
|
+ console.error('提交失败:', result.error)
|
|
|
}
|
|
|
}
|
|
|
-})()
|
|
|
-```
|
|
|
|
|
|
-> **注意**:整个游戏逻辑必须包在 IIFE(立即执行函数)里,避免污染全局作用域。
|
|
|
+ function update(now) {
|
|
|
+ if (ended) return
|
|
|
|
|
|
-### 3. 本地预览
|
|
|
+ const elapsed = now - startedAt
|
|
|
+ score = Math.floor(elapsed / 1000)
|
|
|
|
|
|
-写一个 `index.html`,stub 掉 GameSDK,方便离线调试:
|
|
|
+ if (elapsed >= 30000) {
|
|
|
+ endGame()
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
-```html
|
|
|
-<!DOCTYPE html>
|
|
|
-<html>
|
|
|
-<head>
|
|
|
- <style>
|
|
|
- * { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
- html, body { width: 100%; height: 100%; background: #0a0a0a; }
|
|
|
- #game-container { width: 100%; height: 100%; }
|
|
|
- </style>
|
|
|
-</head>
|
|
|
-<body>
|
|
|
- <div id="game-container"></div>
|
|
|
- <script>
|
|
|
- window.GameSDK = {
|
|
|
- gameId: 'my-game',
|
|
|
- getPlayerName: () => 'dev',
|
|
|
- submit: async (score) => {
|
|
|
- console.log('submit score:', score)
|
|
|
- return { ok: true, rank: 1 }
|
|
|
- }
|
|
|
+ function render() {
|
|
|
+ ctx.fillStyle = '#111'
|
|
|
+ ctx.fillRect(0, 0, width, height)
|
|
|
+
|
|
|
+ ctx.fillStyle = '#fff'
|
|
|
+ ctx.font = '24px sans-serif'
|
|
|
+ ctx.fillText(`score: ${score}`, 20, 40)
|
|
|
+ }
|
|
|
+
|
|
|
+ function loop(now) {
|
|
|
+ update(now)
|
|
|
+ render()
|
|
|
+ requestAnimationFrame(loop)
|
|
|
+ }
|
|
|
+
|
|
|
+ function boot() {
|
|
|
+ if (!resize()) {
|
|
|
+ requestAnimationFrame(boot)
|
|
|
+ return
|
|
|
}
|
|
|
- </script>
|
|
|
- <script src="bundle.js"></script>
|
|
|
-</body>
|
|
|
-</html>
|
|
|
+
|
|
|
+ startedAt = performance.now()
|
|
|
+ requestAnimationFrame(loop)
|
|
|
+ }
|
|
|
+
|
|
|
+ window.addEventListener('resize', resize)
|
|
|
+ boot()
|
|
|
+})()
|
|
|
```
|
|
|
|
|
|
-然后打包并用任意静态服务器预览:
|
|
|
+### 5. 发布
|
|
|
|
|
|
```bash
|
|
|
-npm run build
|
|
|
-npx serve .
|
|
|
+npm run publish
|
|
|
```
|
|
|
|
|
|
-### 4. 打包
|
|
|
+发布成功后,游戏地址为:
|
|
|
|
|
|
-```bash
|
|
|
-npm run build
|
|
|
+```text
|
|
|
+https://loganz2.cn/games/<game-id>
|
|
|
```
|
|
|
|
|
|
-产物为根目录下的 `bundle.js`。
|
|
|
+## 你会用到的两个东西
|
|
|
|
|
|
----
|
|
|
+### 1. 浏览器里的 `GameSDK`
|
|
|
|
|
|
-## 上传到平台
|
|
|
+平台会在加载你的 bundle 前自动注入全局变量:
|
|
|
|
|
|
-### 上传游戏 bundle
|
|
|
+```ts
|
|
|
+GameSDK.gameId
|
|
|
+GameSDK.getPlayerName()
|
|
|
+GameSDK.submit(score)
|
|
|
+```
|
|
|
|
|
|
-```bash
|
|
|
-curl -X POST https://loganz2.cn/api/games/bundle \
|
|
|
- -H "x-game-id: my-game" \
|
|
|
- -H "x-game-name: 我的游戏" \
|
|
|
- -H "x-game-description: 游戏描述" \
|
|
|
- -H "x-game-controls: 方向键移动" \
|
|
|
- -H "content-type: application/javascript" \
|
|
|
- --data-binary "@bundle.js"
|
|
|
+类型:
|
|
|
+
|
|
|
+```ts
|
|
|
+type GameSDKSubmitResult =
|
|
|
+ | { ok: true; rank: number }
|
|
|
+ | { ok: false; error: string }
|
|
|
```
|
|
|
|
|
|
-| Header | 说明 |
|
|
|
-|---|---|
|
|
|
-| `x-game-id` | 游戏唯一 id,只允许字母、数字、`_`、`-` |
|
|
|
-| `x-game-name` | 显示名称,需 URL encode |
|
|
|
-| `x-game-description` | 简短描述(可选),需 URL encode |
|
|
|
-| `x-game-controls` | 操作说明(可选),需 URL encode |
|
|
|
+说明:
|
|
|
+
|
|
|
+1. `gameId` 是当前游戏 id。
|
|
|
+2. `getPlayerName()` 返回玩家昵称。
|
|
|
+3. `submit(score)` 提交一局游戏的最终分数。
|
|
|
+
|
|
|
+### 2. 终端里的 `game-sdk` 命令
|
|
|
|
|
|
-上传成功后游戏立即可用,访问 `https://loganz2.cn/games/<id>` 即可游玩。
|
|
|
+这是发布命令,不是浏览器 API。
|
|
|
|
|
|
-### 上传静态资源(可选)
|
|
|
+它负责:
|
|
|
|
|
|
-如果游戏需要图片、音频等资源:
|
|
|
+1. 上传 `bundle.js`
|
|
|
+2. 上传资源文件
|
|
|
+3. 删除游戏
|
|
|
+
|
|
|
+常用命令:
|
|
|
|
|
|
```bash
|
|
|
-curl -X POST https://loganz2.cn/api/games/asset \
|
|
|
- -H "x-game-id: my-game" \
|
|
|
- -H "x-filename: player.png" \
|
|
|
- --data-binary "@player.png"
|
|
|
+game-sdk publish
|
|
|
+game-sdk asset --file ./player.png
|
|
|
+game-sdk delete --game-id my-game
|
|
|
+```
|
|
|
+
|
|
|
+如果命令参数没有写全,CLI 会继续从这些位置读取配置:
|
|
|
+
|
|
|
+1. 命令行参数
|
|
|
+2. `game-sdk.config.json`
|
|
|
+3. `game.config.json`
|
|
|
+4. `package.json` 里的 `gameSdk`
|
|
|
+
|
|
|
+## 平台运行规则
|
|
|
+
|
|
|
+这些规则必须满足,否则很容易出现空白页、无法开始、尺寸错误、重复提交分数等问题。
|
|
|
+
|
|
|
+### 1. 必须挂载到 `#game-container`
|
|
|
+
|
|
|
+平台页面只提供这个容器:
|
|
|
+
|
|
|
+```html
|
|
|
+<div id="game-container"></div>
|
|
|
+```
|
|
|
+
|
|
|
+你的游戏必须自己把 Canvas、DOM、WebGL 内容挂到这里。
|
|
|
+
|
|
|
+### 2. 推荐把入口包在 IIFE 里
|
|
|
+
|
|
|
+```js
|
|
|
+;(function () {
|
|
|
+ // game code
|
|
|
+})()
|
|
|
+```
|
|
|
+
|
|
|
+这样可以避免污染全局作用域。
|
|
|
+
|
|
|
+### 3. 不要假设容器一开始就有稳定尺寸
|
|
|
+
|
|
|
+平台页面由 React 渲染,容器虽然存在,但你的 bundle 执行时它的尺寸可能还没稳定。
|
|
|
+
|
|
|
+所以:
|
|
|
+
|
|
|
+1. Canvas 游戏必须处理 `resize`。
|
|
|
+2. 读取到 `0` 宽高时要等待下一帧重试。
|
|
|
+3. 每次重设 canvas 尺寸后,要先重置 transform,再重新缩放。
|
|
|
+
|
|
|
+推荐模式:
|
|
|
+
|
|
|
+```js
|
|
|
+function resize() {
|
|
|
+ const rect = container.getBoundingClientRect()
|
|
|
+ const width = rect.width
|
|
|
+ const height = rect.height
|
|
|
+ const dpr = window.devicePixelRatio || 1
|
|
|
+
|
|
|
+ if (width <= 0 || height <= 0) return false
|
|
|
+
|
|
|
+ canvas.width = width * dpr
|
|
|
+ canvas.height = height * dpr
|
|
|
+ ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
|
+ ctx.scale(dpr, dpr)
|
|
|
+ return true
|
|
|
+}
|
|
|
```
|
|
|
|
|
|
-资源上传后可通过以下路径访问:
|
|
|
+### 4. 游戏必须有明确结束条件
|
|
|
+
|
|
|
+平台不会替你判断一局是否结束。
|
|
|
+
|
|
|
+你必须自己定义,例如:
|
|
|
+
|
|
|
+1. 倒计时结束
|
|
|
+2. 生命耗尽
|
|
|
+3. 碰撞失败
|
|
|
+4. 关卡清空
|
|
|
+5. 无法继续操作
|
|
|
+
|
|
|
+结束时应该:
|
|
|
+
|
|
|
+1. 停止本局逻辑
|
|
|
+2. 固化最终分数
|
|
|
+3. 调用一次 `GameSDK.submit(finalScore)`
|
|
|
+
|
|
|
+### 5. `submit` 只在一局结束时调用一次
|
|
|
|
|
|
+不要这样用:
|
|
|
+
|
|
|
+1. 每得 1 分就提交
|
|
|
+2. 在渲染循环里提交
|
|
|
+3. 同一个结束条件反复触发时重复提交
|
|
|
+
|
|
|
+推荐加保护:
|
|
|
+
|
|
|
+```js
|
|
|
+let submitted = false
|
|
|
+
|
|
|
+async function endGame(score) {
|
|
|
+ if (submitted) return
|
|
|
+ submitted = true
|
|
|
+ await GameSDK.submit(score)
|
|
|
+}
|
|
|
```
|
|
|
-/api/games/<id>/assets/<filename>
|
|
|
+
|
|
|
+### 6. 资源必须走平台绝对路径
|
|
|
+
|
|
|
+资源上传后,运行时请用:
|
|
|
+
|
|
|
+```text
|
|
|
+/api/games/<game-id>/assets/<filename>
|
|
|
```
|
|
|
|
|
|
-在游戏代码里直接用绝对路径加载:
|
|
|
+例如:
|
|
|
|
|
|
```js
|
|
|
const img = new Image()
|
|
|
img.src = '/api/games/my-game/assets/player.png'
|
|
|
```
|
|
|
|
|
|
-### 更新游戏
|
|
|
+不要依赖本地相对路径结构。
|
|
|
+
|
|
|
+## 发布命令
|
|
|
+
|
|
|
+### 发布 bundle
|
|
|
+
|
|
|
+如果配置文件已经写好,直接:
|
|
|
+
|
|
|
+```bash
|
|
|
+game-sdk publish
|
|
|
+```
|
|
|
+
|
|
|
+也可以显式传参:
|
|
|
+
|
|
|
+```bash
|
|
|
+game-sdk publish \
|
|
|
+ --game-id my-game \
|
|
|
+ --name "我的游戏" \
|
|
|
+ --bundle ./bundle.js \
|
|
|
+ --description "游戏描述" \
|
|
|
+ --controls "方向键移动"
|
|
|
+```
|
|
|
+
|
|
|
+参数:
|
|
|
+
|
|
|
+1. `--game-id`:必填。只允许字母、数字、`_`、`-`。
|
|
|
+2. `--name`:必填。平台展示名称。
|
|
|
+3. `--bundle`:必填。bundle 文件路径。
|
|
|
+4. `--description`:可选。简短描述。
|
|
|
+5. `--controls`:可选。操作说明。
|
|
|
+
|
|
|
+### 上传资源
|
|
|
+
|
|
|
+```bash
|
|
|
+game-sdk asset --game-id my-game --file ./player.png
|
|
|
+```
|
|
|
+
|
|
|
+或者:
|
|
|
+
|
|
|
+```bash
|
|
|
+game-sdk asset --file ./player.png
|
|
|
+```
|
|
|
+
|
|
|
+上面这个简写成立的前提是 `game-id` 已经写进配置文件。
|
|
|
|
|
|
-重新上传 bundle 即可,同一个 id 会直接覆盖。
|
|
|
+如果你想指定平台上的文件名:
|
|
|
+
|
|
|
+```bash
|
|
|
+game-sdk asset \
|
|
|
+ --game-id my-game \
|
|
|
+ --file ./assets/player-v2.png \
|
|
|
+ --name player.png
|
|
|
+```
|
|
|
|
|
|
### 删除游戏
|
|
|
|
|
|
```bash
|
|
|
-curl -X DELETE https://loganz2.cn/api/games/my-game
|
|
|
+game-sdk delete --game-id my-game
|
|
|
+```
|
|
|
+
|
|
|
+## 本地离线预览
|
|
|
+
|
|
|
+本地只能 stub `window.GameSDK` 来调试游戏逻辑,不代表本地部署平台。
|
|
|
+
|
|
|
+`index.html`:
|
|
|
+
|
|
|
+```html
|
|
|
+<!doctype html>
|
|
|
+<html>
|
|
|
+ <head>
|
|
|
+ <meta charset="UTF-8" />
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
+ <style>
|
|
|
+ * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+ html, body { width: 100%; height: 100%; background: #0a0a0a; }
|
|
|
+ #game-container { width: 100%; height: 100%; }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+ <div id="game-container"></div>
|
|
|
+ <script>
|
|
|
+ window.GameSDK = {
|
|
|
+ gameId: 'my-game',
|
|
|
+ getPlayerName: function () { return 'dev' },
|
|
|
+ submit: async function (score) {
|
|
|
+ console.log('submit score:', score)
|
|
|
+ return { ok: true, rank: 1 }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+ <script src="./bundle.js"></script>
|
|
|
+ </body>
|
|
|
+</html>
|
|
|
+```
|
|
|
+
|
|
|
+预览:
|
|
|
+
|
|
|
+```bash
|
|
|
+npm run build
|
|
|
+npx serve .
|
|
|
```
|
|
|
+
|
|
|
+## 常见错误
|
|
|
+
|
|
|
+### 页面空白,但 bundle 已上传
|
|
|
+
|
|
|
+优先检查:
|
|
|
+
|
|
|
+1. 是否真的挂载到了 `#game-container`
|
|
|
+2. 是否读取到了 `0` 尺寸后就再也没重试
|
|
|
+3. 是否多次 resize 后重复缩放了 canvas 坐标系
|
|
|
+4. 是否有运行时错误导致第一帧前中断
|
|
|
+
|
|
|
+### 分数反复提交
|
|
|
+
|
|
|
+通常是因为:
|
|
|
+
|
|
|
+1. 在动画循环里调用了 `GameSDK.submit`
|
|
|
+2. 没有 `submitted` 保护
|
|
|
+3. 同一个结束条件被重复触发
|
|
|
+
|
|
|
+### 资源 404
|
|
|
+
|
|
|
+通常是因为:
|
|
|
+
|
|
|
+1. 资源没有上传
|
|
|
+2. 路径不是 `/api/games/<id>/assets/<filename>`
|
|
|
+3. 上传文件名和代码里使用的文件名不一致
|
|
|
+
|
|
|
+## 最后
|
|
|
+
|
|
|
+这个 SDK 故意保持很薄。
|
|
|
+
|
|
|
+平台只关心三件事:
|
|
|
+
|
|
|
+1. 你的游戏能在 `#game-container` 里正确运行
|
|
|
+2. 一局结束时能提交最终分数
|
|
|
+3. 你能稳定地发布 bundle 和资源
|
|
|
+
|
|
|
+其余渲染、输入、状态机、资源管理,都由游戏自己决定。
|