数字/事实:一款中等手游可能有百万级玩家档案、实时排行榜每秒更新、对战房间要毫秒级状态同步。这些需求里,玩家档案、好友关系、排行榜这类"数据为主"的部分 Supabase 能很好覆盖;但帧级对战状态同步得用专用游戏服务。这一节我们讲游戏的"数据层"怎么用 Supabase 搭,以及边界。
玩家档案本质是带扩展字段的用户表,好友关系是多对多。这两者都是关系模型强项:
-- 玩家档案,复用 auth.users 的 id create table public.players ( id uuid primary key references auth.users(id), nickname text unique not null, level int default 1, coins int default 0 ); -- 好友关系(双向,用复合主键去重) create table public.friendships ( a uuid references public.players(id), b uuid references public.players(id), primary key (a, b), created_at timestamptz default now() ); alter table public.players enable row level security; alter table public.friendships enable row level security; -- 玩家可读自己档案,好友可读彼此公开信息 create policy "自己档案" on public.players for all using (auth.uid() = id) with check (auth.uid() = id); create policy "好友互看" on public.players for select using ( exists ( select 1 from public.friendships f where (f.a = auth.uid() and f.b = players.id) or (f.b = auth.uid() and f.a = players.id) ) );
排行榜是"按分数排序取前 N",高频读。给分数建索引,查询走有序扫描:
create index idx_players_score on public.players (coins desc); -- 取前 100 名 select nickname, coins from public.players order by coins desc limit 100;
若分数更新极频繁、榜单又大,可用物化视图定时刷新,避免每次实时排序全表。
聊天那种"数据变了推一下"的实时,Supabase Realtime 胜任。但动作游戏的"每帧坐标同步"是另一回事——要求毫秒级、高频、状态机复杂,应交给专用游戏服务器(如基于 UDP 的帧同步服务),Supabase 只存最终战绩。
下面 SVG 区分"Supabase 能管的游戏数据"和"要专用服务的部分":

背景:一个消除类休闲游戏要做"好友排行榜"和国家榜,玩家数据要安全、榜单要快。
操作过程:
players 行(昵称来自第三方登录元数据)。update players set coins = coins + 奖励 where id = auth.uid(),RLS 保证只能改自己。select ... order by coins desc limit 100 走索引,毫秒返回。friendships 拿到好友 id 列表,再 where id in (...) 取分数,前端并排展示。结果:数据层零自建服务,榜单流畅;真正需要低延迟的部分才引入专用组件,成本可控。
解读:游戏后端常被误以为"必须全套自研"。其实数据为主的部分(档案、社交、榜单、存档)用 Supabase 又快又安全,只有实时战斗这种硬实时需求才需要专用服务。把两者切开,省钱省心。
变式:若担心分数被客户端篡改(直接 update coins),可把"加分数"做成数据库函数,由服务端/受信函数调用,客户端只能触发"过关"事件,不能自己写分数——把信任边界收进库。
-- 受信函数:只有 security definer + 服务端调用才能改分数 create or replace function public.award_coins(p_player uuid, p_delta int) returns void language plpgsql security definer set search_path = public as $$ begin -- 校验增量合理(比如单关奖励不超过上限,防刷) if p_delta < 0 or p_delta > 1000 then raise exception '非法奖励值'; end if; update public.players set coins = coins + p_delta where id = p_player; end; $$; -- 客户端只能调函数,不能直接 update 列 revoke update on public.players from anon, authenticated;
security definer 让函数以定义者权限跑,配合 revoke update,玩家即使绕过客户端也改不了 coins 列,只能走 award_coins 这个被校验过的入口。里面的 p_delta 上限校验是第二道防线:即便函数被高频调用,单次加分也封顶,刷分收益有限。
下面一张表区分"哪些游戏子系统该放 Supabase、哪些该接专用服务",避免你一股脑全塞进数据库:
| 游戏子系统 | 放 Supabase? | 理由 |
|---|---|---|
| 玩家档案 / 好友关系 | 是 | 关系模型强项,RLS 控权限 |
| 排行榜 / 成就 | 是 | 索引或物化视图够快 |
| 关卡存档 | 是 | 低频写、按用户隔离 |
| 回合制对战状态 | 可(broadcast) | 秒级同步,Realtime 胜任 |
| 帧级动作同步 | 否 | 要 UDP、毫秒级、专用服务器 |
| 匹配撮合 | 否 | 需要内存态 + 低延迟调度 |
⚠️ revoke update 后别忘了保留"读"和"插入存档"的权限给玩家,否则正常功能也被锁死。权限收紧要精确到列和角色,先列清楚"玩家自己能做什么",再 revoke 多余的那一项,别图省事整表锁掉。
💡 关键直觉:游戏的信任边界不该画在"客户端是否诚实",而要画在"数据库入口是否可被绕过"。把数值变更收进受信函数、列权限收回,等于把作弊成本从"改前端"提高到"攻破数据库",绝大多数情况下这就足够拦住刷分。
⚠️ 别让客户端直接 update coins,等于把分数修改权交给玩家。涉及数值变更用受信函数(服务端/Edge Function 调)封装,客户端只发"事件"不写结果。
💡 我们建议游戏的"数据平面"全交给 Supabase,"实时战斗平面"交给专用服务,两者通过战绩回写衔接。这样你不用为维护玩家数据库分心,专注玩法。
下一节我们剖几个真实成功案例,看前面所有知识点怎么在完整产品里组装落地。