← ユースケース

ゲームのオークションハウスとプレイヤーマーケット

出品・入札・カウントダウンを全クライアントにリアルタイム表示。終了間際の入札は自動延長でスナイプを防ぎ、即決と落札価格の推移にも対応。入札の判断はあなたのサーバー、通貨とアイテムはゲーム側が管理します。

オークションハウスや取引所、プレイヤーマーケットはゲーム内経済を回しますが、公平に感じられるのは全員が同じものを同時に見られるときだけです。入札された瞬間に新しい価格が見え、どの画面でもカウントダウンが同じで、最後の 1 秒で横取りされないこと。オープンソースの @hibira-io/auction は、これを Hibira の上で実現し、すべての入札はあなたのサーバーが判断します。

プレイヤーが得られるもの

  • 出品・価格の変化・終了が、開いているすべてのクライアントに再読み込みなしで反映されます。
  • カウントダウンはサーバーの時計に合わせて進み、終了間際の入札はスナイプではなく延長になります。
  • 上回られた瞬間の通知、即決購入、アイテムごとの最近の落札価格。
オークションハウスの例:プレイヤーが狩人の弓に入札し、すぐに別のウィンドウが上回る入札をすると、最初のプレイヤーに新しい価格と通知が表示されます。
オークションハウスの例:プレイヤーが狩人の弓に入札し、すぐに別のウィンドウが上回る入札をすると、最初のプレイヤーに新しい価格と通知が表示されます。

使っている機能

  • ハウスごとに公開またはプライベートのチャンネル 1 つ(auction-main)。イベントは auction-update の 1 種類で、出品の全体とバージョン番号を運びます。
  • サーバーからの HTTP API(trigger)。見ているプレイヤーが何人でも、変更ごとに 1 回だけ配信します。
  • @hibira-io/auction:サーバー側は入札のチェック、終了間際の延長、落札処理を行い、クライアント側は各出品の最新版を保持して再接続後に追いつきます。
プレイヤー AあなたのサーバーHibiraプレイヤー BPOST /auction/:id/bid120 ゴールドhouse.bid() が入札をチェックしてゴールドを確保auction-update価格 120、A が最高入札auction-updateauction-update「入札 126」
入札はあなたのサーバーへ、通知は Hibira から全員へ

ステップ 1:サーバーでハウスを作る

アイテムと通貨はゲーム側が管理したままです。ハウスは、入札者の通貨の確保、上回られたときの返却、終了時のアイテムと通貨の移動をあなたに依頼します。

import Pusher from 'pusher';
import { createAuctionHouse } from '@hibira-io/auction/server';

const hibira = new Pusher({ appId, key, secret, host: 'ws.hibira.io', useTLS: true });

const house = createAuctionHouse({
  pusher: hibira,
  name: 'main',
  extendWithin: 30000, // a bid in the last 30 s adds time
  reserve: async ({ userId, amount }) => {
    if (!(await wallet.hold(userId, amount))) throw new Error('not enough gold');
  },
  release: async ({ userId, amount }) => wallet.unhold(userId, amount),
  settle: async ({ listing }) => game.deliver(listing), // idempotent by listing.id
});
house.start(); // closes and settles ended auctions

ステップ 2:4 つのエンドポイント

プレイヤーはリクエスト本文からではなく、必ず自分のセッションから取り出してください。

app.get('/auction', async (req, res) => res.json(await house.snapshot()));

app.post('/auction/:id/bid', requireLogin, async (req, res) => {
  try {
    res.json(await house.bid({ listingId: req.params.id, bidderId: req.user.id, bidder: { name: req.user.name }, amount: req.body.amount }));
  } catch (err) {
    res.status(err.status ?? 500).json({ error: err.code });
  }
});
// The same for house.buyout() and house.list()

ステップ 3:ゲームに表示する

import { AuctionHouse } from '@hibira-io/auction';

const house = AuctionHouse.open(pusher, 'main', {
  load: () => fetch('/auction').then((r) => r.json()),
});

house.on('change', ({ listings }) => render(listings));
setInterval(() => listings.forEach((l) => showTimeLeft(l, house.timeLeft(l))), 250);

bidButton.onclick = () => fetch(`/auction/${l.id}/bid`, { method: 'POST', body: JSON.stringify({ amount: l.minBid }) });

house.prices(type) はアイテムの種類ごとの最近の落札価格を返すので、価格チャートに使えます。再接続後はスナップショットを読み直すため、取りこぼしはありません。

試してみる

オークションハウスの例は、NPC 商人、バッグ、即決、上回り通知、落札価格を備えた Next.js のゲーム内マーケットです(7 言語対応)。2 つのウィンドウで開いて、片方で入札してみてください。

ヒント

  • 見ているプレイヤーが何人でも、変更 1 回につき 1 メッセージです。大きなハウスはカテゴリや地域で分けると(name: 'weapons')、各クライアントは表示する分だけを読み込めます。
  • ゲームサーバーが複数あるときは、データベース上の store を渡してください。保存は比較してから書き込むため、2 台のサーバーが同じ入札を両方受け付けることはありません。
  • 動かすのはゲーム内の通貨とアイテムで、現実のお金は扱いません。
  • Unity、Godot、その他の Pusher SDK のクライアントも同じ auction-main チャンネルを購読し、同じエンドポイントを呼べます。ルームとマッチメイキングはマルチプレイヤーのゲームルームとロビーをご覧ください。