Game/GameSfx.cs
using System;

/// <summary>
/// 游戏音效(M4):**全部 2D 平面声(.sound UI 标志,用户定稿——不随距离衰减)**。
/// 吃食物这类高频音只播"本机耳朵"自己的(IsMine 判定),死亡/爆裂全场都播(低频事件)。
/// 资产未就绪(导入中/客户端未下载)时静默跳过,绝不让音效调用打断游戏循环。
/// (曾名 GameSound:创建时与编辑器写入竞态把编译缓存搞花,换名重建——内容与旧版一致)
/// </summary>
public static class GameSfx
{
	const string SfxEat = "sounds/cr_eat.sound";
	const string SfxEatBall = "sounds/cr_eat_ball.sound";
	const string SfxPop = "sounds/cr_pop.sound";
	const string SfxSplit = "sounds/cr_split.sound";
	const string SfxEject = "sounds/cr_eject.sound";
	const string SfxRespawn = "sounds/cr_respawn.sound";

	static int _eatCount;   // 本局吃食物计数(每 10 分播一次音,重生清零)

	/// <summary> 该 owner 的事件是否归"本机耳朵"管:host 比对 SteamId;客户端比对
	/// 本机球的 OwnerSteamId(双开合成 ID 与 Game.SteamId 对不上,只能走球上同步值) </summary>
	public static bool IsMine( long ownerSteamId )
	{
		if ( NetworkManager.IsAuthority )
			return ownerSteamId == Game.SteamId.Value;

		var b = CircleroyaleGame.Current?.LocalBall;
		return b.IsValid() && b.OwnerSteamId == ownerSteamId;
	}

	/// <summary> 吃食物:每累计 10 分播一次(不再每颗都播,避免机关枪)。重生时 ResetEatCount 清零 </summary>
	public static void EatFood( Vector3 pos )
	{
		if ( ++_eatCount % 10 != 0 ) return;
		Play( SfxEat, pos );
	}

	/// <summary> 重生时清零吃食物计数(下一条命重新从 0 计) </summary>
	public static void ResetEatCount() => _eatCount = 0;

	public static void EatBall( Vector3 pos ) => Play( SfxEatBall, pos );

	public static void Pop( Vector3 pos ) => Play( SfxPop, pos );

	public static void Split( Vector3 pos ) => Play( SfxSplit, pos );

	public static void Eject( Vector3 pos ) => Play( SfxEject, pos );

	public static void Respawn( Vector3 pos ) => Play( SfxRespawn, pos );

	static void Play( string path, Vector3 pos )
	{
		try
		{
			Sound.Play( path, pos );
		}
		catch
		{
			// 资源未就绪:静默跳过(不刷日志)
		}
	}
}