@musashishao/agent-kit 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/agents/ai-asset-factory.md +700 -0
- package/.agent/agents/ai-audio-factory.md +503 -0
- package/.agent/agents/game-developer.md +4 -4
- package/.agent/agents/orchestrator.md +113 -3
- package/.agent/agents/project-planner.md +67 -0
- package/.agent/agents/unity-mobile-master.md +949 -0
- package/.agent/mcp/config/registry.json +65 -51
- package/.agent/mcp/servers/notebooklm/README.md +114 -0
- package/.agent/mcp/servers/notebooklm/package.json +35 -0
- package/.agent/mcp/servers/notebooklm/src/auth/chrome.ts +225 -0
- package/.agent/mcp/servers/notebooklm/src/auth/index.ts +1 -0
- package/.agent/mcp/servers/notebooklm/src/index.ts +516 -0
- package/.agent/mcp/servers/notebooklm/src/services/index.ts +3 -0
- package/.agent/mcp/servers/notebooklm/src/services/library.ts +217 -0
- package/.agent/mcp/servers/notebooklm/src/services/notebooklm.ts +380 -0
- package/.agent/mcp/servers/notebooklm/tsconfig.json +15 -0
- package/.agent/mcp-gateway/README.md +169 -20
- package/.agent/mcp-gateway/package.json +22 -7
- package/.agent/mcp-gateway/src/auth/index.ts +55 -0
- package/.agent/mcp-gateway/src/auth/middleware.ts +242 -0
- package/.agent/mcp-gateway/src/auth/oauth.ts +462 -0
- package/.agent/mcp-gateway/src/auth/scopes.ts +227 -0
- package/.agent/mcp-gateway/src/index.ts +252 -105
- package/.agent/mcp-gateway/src/observability/index.ts +5 -0
- package/.agent/mcp-gateway/src/observability/otel.ts +405 -0
- package/.agent/mcp-gateway/src/transports/index.ts +5 -0
- package/.agent/mcp-gateway/src/transports/streamableHttp.ts +235 -0
- package/.agent/rules/CODEX.md +89 -0
- package/.agent/rules/CODE_RULES.md +73 -0
- package/.agent/rules/GEMINI.md +25 -0
- package/.agent/rules/MEMORY_STATE.md +110 -0
- package/.agent/rules/REFERENCE.md +33 -141
- package/.agent/rules/REF_SKILLS.md +116 -0
- package/.agent/rules/REF_WORKFLOWS.md +81 -0
- package/.agent/scripts/ak_cli.py +106 -5
- package/.agent/scripts/memory_manager.py +48 -9
- package/.agent/skills/anti-hallucination/SKILL.md +295 -0
- package/.agent/skills/anti-hallucination/scripts/check_hallucination.py +299 -0
- package/.agent/skills/bifurcation-analysis/SKILL.md +56 -0
- package/.agent/skills/brainstorming/SKILL.md +80 -6
- package/.agent/skills/decision-memory/SKILL.md +317 -0
- package/.agent/skills/emergence-detector/SKILL.md +230 -0
- package/.agent/skills/emergence-detector/scripts/check_emergence.py +265 -0
- package/.agent/skills/explained-qa/SKILL.md +142 -0
- package/.agent/skills/explained-qa/game-terminology.md +214 -0
- package/.agent/skills/game-development/ai-dialogue-engine/SKILL.md +442 -0
- package/.agent/skills/game-development/ai-graphics-generator/SKILL.md +463 -0
- package/.agent/skills/game-development/ai-playtest-framework/SKILL.md +570 -0
- package/.agent/skills/game-development/camera-systems/SKILL.md +607 -0
- package/.agent/skills/game-development/card-battle-engine/SKILL.md +618 -0
- package/.agent/skills/game-development/character-controller-3d/SKILL.md +908 -0
- package/.agent/skills/game-development/cloud-save-sync/SKILL.md +527 -0
- package/.agent/skills/game-development/combat-system/SKILL.md +748 -0
- package/.agent/skills/game-development/compliance-rating/SKILL.md +277 -0
- package/.agent/skills/game-development/crossplatform-build/SKILL.md +386 -0
- package/.agent/skills/game-development/cultivation-progression/SKILL.md +520 -0
- package/.agent/skills/game-development/data-driven-balance/SKILL.md +535 -0
- package/.agent/skills/game-development/game-analytics-integrator/SKILL.md +410 -0
- package/.agent/skills/game-development/game-audio-advanced/SKILL.md +646 -0
- package/.agent/skills/game-development/game-economy-designer/SKILL.md +375 -0
- package/.agent/skills/game-development/game-marketing/SKILL.md +85 -0
- package/.agent/skills/game-development/game-state-manager/SKILL.md +883 -0
- package/.agent/skills/game-development/hybrid-game-spec/SKILL.md +220 -0
- package/.agent/skills/game-development/inventory-quest/SKILL.md +747 -0
- package/.agent/skills/game-development/liveops/SKILL.md +308 -0
- package/.agent/skills/game-development/localization/SKILL.md +286 -0
- package/.agent/skills/game-development/mobile-input-patterns/SKILL.md +343 -0
- package/.agent/skills/game-development/monetization-strategy/SKILL.md +94 -0
- package/.agent/skills/game-development/multiplayer-master/SKILL.md +727 -0
- package/.agent/skills/game-development/narrative-branching/SKILL.md +593 -0
- package/.agent/skills/game-development/procedural-level-ai/SKILL.md +367 -0
- package/.agent/skills/game-development/prototyping-rapid/SKILL.md +205 -0
- package/.agent/skills/game-development/spec-ecosystem/SKILL.md +155 -0
- package/.agent/skills/game-development/spec-ecosystem/decision-log-format.md +129 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/PLAN-template.md +178 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/SPEC-template.md +110 -0
- package/.agent/skills/game-development/spec-ecosystem/templates/TASKS-template.md +156 -0
- package/.agent/skills/game-development/survival-systems/SKILL.md +493 -0
- package/.agent/skills/game-development/testing-qa/SKILL.md +270 -0
- package/.agent/skills/game-development/unity-mobile-optimization/SKILL.md +271 -0
- package/.agent/skills/intent-capture/SKILL.md +65 -0
- package/.agent/skills/mcp-composition/SKILL.md +362 -0
- package/.agent/skills/mcp-observability/SKILL.md +323 -0
- package/.agent/skills/mcp-security/SKILL.md +314 -0
- package/.agent/skills/trust-spectrum/SKILL.md +291 -0
- package/.agent/skills/vibe-coding-guard/SKILL.md +328 -0
- package/.agent/templates/AGENTS.game.md +63 -0
- package/.agent/templates/docs/WORKFLOW_GUIDE.en.md +100 -0
- package/.agent/templates/docs/WORKFLOW_GUIDE.vi.md +100 -0
- package/.agent/workflows/ai-agent.md +2 -0
- package/.agent/workflows/autofix.md +1 -0
- package/.agent/workflows/brainstorm.md +1 -0
- package/.agent/workflows/context.md +1 -0
- package/.agent/workflows/create.md +39 -8
- package/.agent/workflows/dashboard.md +1 -0
- package/.agent/workflows/debug.md +14 -0
- package/.agent/workflows/deploy.md +14 -0
- package/.agent/workflows/enhance.md +44 -0
- package/.agent/workflows/gamekit-init.md +177 -0
- package/.agent/workflows/gamekit-launch.md +338 -0
- package/.agent/workflows/gamekit-plan.md +204 -0
- package/.agent/workflows/gamekit-qa.md +153 -0
- package/.agent/workflows/gamekit-spec.md +243 -0
- package/.agent/workflows/gamekit-tasks.md +208 -0
- package/.agent/workflows/marketing.md +2 -0
- package/.agent/workflows/next.md +1 -0
- package/.agent/workflows/orchestrate.md +12 -0
- package/.agent/workflows/pentest.md +2 -0
- package/.agent/workflows/plan.md +42 -0
- package/.agent/workflows/preview.md +1 -0
- package/.agent/workflows/quality.md +1 -0
- package/.agent/workflows/saas.md +2 -0
- package/.agent/workflows/spec.md +42 -0
- package/.agent/workflows/status.md +1 -0
- package/.agent/workflows/test.md +14 -0
- package/.agent/workflows/ui-ux-pro-max.md +1 -0
- package/bin/cli.js +411 -111
- package/package.json +1 -2
- package/.agent/agents/game-asset-curator.md +0 -317
- package/.agent/agents/game-narrative-designer.md +0 -310
- package/.agent/agents/game-qa-agent.md +0 -441
- package/.agent/workflows/game-prototype.md +0 -154
- package/docs/AI_DATA_INFRASTRUCTURE.md +0 -288
- package/docs/CHANGELOG_AI_INFRA.md +0 -141
- package/docs/MIGRATION_GUIDE_V1.9.md +0 -55
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: multiplayer-master
|
|
3
|
+
description: Complete multiplayer game development. Unity Netcode, Photon Fusion, Mirror, real-time sync, lag compensation, matchmaking, and anti-cheat patterns.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Grep, WebSearch
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Multiplayer Master Skill
|
|
8
|
+
|
|
9
|
+
> Build production-ready multiplayer games with proper networking architecture.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## When to Use
|
|
14
|
+
|
|
15
|
+
- Building real-time multiplayer games
|
|
16
|
+
- Implementing player synchronization
|
|
17
|
+
- Setting up dedicated servers
|
|
18
|
+
- Creating matchmaking systems
|
|
19
|
+
- Handling lag compensation
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Architecture Selection
|
|
24
|
+
|
|
25
|
+
### Decision Matrix
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
What type of multiplayer?
|
|
29
|
+
│
|
|
30
|
+
├── Real-time Competitive (FPS, Fighting)
|
|
31
|
+
│ └── Dedicated Server + Netcode/Photon Fusion
|
|
32
|
+
│ └── Tick rate: 64-128 Hz
|
|
33
|
+
│ └── Lag compensation: CRITICAL
|
|
34
|
+
│
|
|
35
|
+
├── Real-time Cooperative (Co-op, MMO)
|
|
36
|
+
│ └── Dedicated Server or Host-based
|
|
37
|
+
│ └── Tick rate: 20-60 Hz
|
|
38
|
+
│ └── Lag compensation: IMPORTANT
|
|
39
|
+
│
|
|
40
|
+
├── Turn-based (Card games, Strategy)
|
|
41
|
+
│ └── Client-Server REST/WebSocket
|
|
42
|
+
│ └── No tick rate needed
|
|
43
|
+
│ └── Lag compensation: NOT NEEDED
|
|
44
|
+
│
|
|
45
|
+
└── Casual/Social
|
|
46
|
+
└── Host-based (P2P)
|
|
47
|
+
└── Tick rate: 10-30 Hz
|
|
48
|
+
└── Lag compensation: MINIMAL
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Framework Comparison
|
|
52
|
+
|
|
53
|
+
| Framework | Best For | Pros | Cons |
|
|
54
|
+
|-----------|----------|------|------|
|
|
55
|
+
| **Netcode for GameObjects** | Unity-first | Official, well documented | Learning curve |
|
|
56
|
+
| **Photon Fusion** | Fast iteration | Great tools, cloud hosting | Costs at scale |
|
|
57
|
+
| **Mirror** | Self-hosted | Free, flexible | More manual work |
|
|
58
|
+
| **FishNet** | Performance | Fast, modern | Smaller community |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Unity Netcode for GameObjects
|
|
63
|
+
|
|
64
|
+
### Project Setup
|
|
65
|
+
|
|
66
|
+
```csharp
|
|
67
|
+
// 1. Install via Package Manager:
|
|
68
|
+
// com.unity.netcode.gameobjects
|
|
69
|
+
|
|
70
|
+
// 2. Create NetworkManager
|
|
71
|
+
public class GameNetworkManager : NetworkManager
|
|
72
|
+
{
|
|
73
|
+
public static GameNetworkManager Instance { get; private set; }
|
|
74
|
+
|
|
75
|
+
private void Awake()
|
|
76
|
+
{
|
|
77
|
+
Instance = this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public override void OnServerStarted()
|
|
81
|
+
{
|
|
82
|
+
Debug.Log("Server started!");
|
|
83
|
+
NetworkManager.Singleton.SceneManager.LoadScene("GameScene", LoadSceneMode.Single);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public override void OnClientConnectedCallback(ulong clientId)
|
|
87
|
+
{
|
|
88
|
+
Debug.Log($"Client {clientId} connected");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### NetworkObject & NetworkBehaviour
|
|
94
|
+
|
|
95
|
+
```csharp
|
|
96
|
+
public class PlayerController : NetworkBehaviour
|
|
97
|
+
{
|
|
98
|
+
[Header("Movement")]
|
|
99
|
+
[SerializeField] private float moveSpeed = 5f;
|
|
100
|
+
|
|
101
|
+
// Synced variable - server authoritative
|
|
102
|
+
private NetworkVariable<Vector3> _networkPosition = new NetworkVariable<Vector3>(
|
|
103
|
+
readPerm: NetworkVariableReadPermission.Everyone,
|
|
104
|
+
writePerm: NetworkVariableWritePermission.Server
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
private NetworkVariable<int> _health = new NetworkVariable<int>(100);
|
|
108
|
+
|
|
109
|
+
public override void OnNetworkSpawn()
|
|
110
|
+
{
|
|
111
|
+
if (IsOwner)
|
|
112
|
+
{
|
|
113
|
+
// Setup local player
|
|
114
|
+
Camera.main.GetComponent<CameraFollow>().SetTarget(transform);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_health.OnValueChanged += OnHealthChanged;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
void Update()
|
|
121
|
+
{
|
|
122
|
+
if (!IsOwner) return;
|
|
123
|
+
|
|
124
|
+
// Client-side input
|
|
125
|
+
float h = Input.GetAxisRaw("Horizontal");
|
|
126
|
+
float v = Input.GetAxisRaw("Vertical");
|
|
127
|
+
Vector3 moveInput = new Vector3(h, 0, v).normalized;
|
|
128
|
+
|
|
129
|
+
if (moveInput.magnitude > 0.1f)
|
|
130
|
+
{
|
|
131
|
+
MoveServerRpc(moveInput);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
[ServerRpc]
|
|
136
|
+
private void MoveServerRpc(Vector3 direction)
|
|
137
|
+
{
|
|
138
|
+
// Server validates and applies movement
|
|
139
|
+
Vector3 newPos = transform.position + direction * moveSpeed * Time.deltaTime;
|
|
140
|
+
|
|
141
|
+
// Validate movement (anti-cheat)
|
|
142
|
+
if (IsValidMovement(transform.position, newPos))
|
|
143
|
+
{
|
|
144
|
+
transform.position = newPos;
|
|
145
|
+
_networkPosition.Value = newPos;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private bool IsValidMovement(Vector3 from, Vector3 to)
|
|
150
|
+
{
|
|
151
|
+
float maxDistance = moveSpeed * Time.deltaTime * 1.5f; // Allow some tolerance
|
|
152
|
+
return Vector3.Distance(from, to) <= maxDistance;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private void OnHealthChanged(int oldValue, int newValue)
|
|
156
|
+
{
|
|
157
|
+
// Update UI
|
|
158
|
+
UIManager.Instance.UpdateHealthBar(newValue);
|
|
159
|
+
|
|
160
|
+
if (newValue <= 0)
|
|
161
|
+
{
|
|
162
|
+
HandleDeath();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
[ServerRpc]
|
|
167
|
+
public void TakeDamageServerRpc(int damage, ServerRpcParams rpcParams = default)
|
|
168
|
+
{
|
|
169
|
+
_health.Value -= damage;
|
|
170
|
+
|
|
171
|
+
// Notify all clients of damage
|
|
172
|
+
TakeDamageClientRpc(damage);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
[ClientRpc]
|
|
176
|
+
private void TakeDamageClientRpc(int damage)
|
|
177
|
+
{
|
|
178
|
+
// Play damage effects on all clients
|
|
179
|
+
PlayDamageEffect();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### NetworkTransform (Position Sync)
|
|
185
|
+
|
|
186
|
+
```csharp
|
|
187
|
+
// For smooth interpolation, use NetworkTransform component
|
|
188
|
+
// Settings for different game types:
|
|
189
|
+
|
|
190
|
+
// FPS/Action Game:
|
|
191
|
+
// - Interpolate: ON
|
|
192
|
+
// - Sync Position: X, Y, Z
|
|
193
|
+
// - Sync Rotation: Y only (or X,Y for full aim)
|
|
194
|
+
// - Threshold: 0.001 (high precision)
|
|
195
|
+
|
|
196
|
+
// Casual Game:
|
|
197
|
+
// - Interpolate: ON
|
|
198
|
+
// - Sync Position: X, Z (no Y if grounded)
|
|
199
|
+
// - Threshold: 0.1 (lower precision OK)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Photon Fusion
|
|
205
|
+
|
|
206
|
+
### Setup
|
|
207
|
+
|
|
208
|
+
```csharp
|
|
209
|
+
// 1. Install Photon Fusion from Asset Store
|
|
210
|
+
// 2. Create App ID at photonengine.com
|
|
211
|
+
|
|
212
|
+
public class FusionBootstrap : MonoBehaviour
|
|
213
|
+
{
|
|
214
|
+
[SerializeField] private NetworkRunner _runnerPrefab;
|
|
215
|
+
|
|
216
|
+
async void Start()
|
|
217
|
+
{
|
|
218
|
+
var runner = Instantiate(_runnerPrefab);
|
|
219
|
+
|
|
220
|
+
await runner.StartGame(new StartGameArgs
|
|
221
|
+
{
|
|
222
|
+
GameMode = GameMode.AutoHostOrClient,
|
|
223
|
+
SessionName = "MyGame",
|
|
224
|
+
Scene = SceneManager.GetActiveScene().buildIndex,
|
|
225
|
+
SceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>()
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### NetworkBehaviour (Fusion style)
|
|
232
|
+
|
|
233
|
+
```csharp
|
|
234
|
+
public class FusionPlayer : NetworkBehaviour
|
|
235
|
+
{
|
|
236
|
+
[Networked] public int Health { get; set; } = 100;
|
|
237
|
+
[Networked] public NetworkString<_32> PlayerName { get; set; }
|
|
238
|
+
|
|
239
|
+
// Tick-aligned input
|
|
240
|
+
[Networked] private NetworkButtons _buttons { get; set; }
|
|
241
|
+
|
|
242
|
+
public override void FixedUpdateNetwork()
|
|
243
|
+
{
|
|
244
|
+
if (GetInput(out NetworkInputData input))
|
|
245
|
+
{
|
|
246
|
+
// Process input (runs on both client and server)
|
|
247
|
+
Vector3 move = new Vector3(input.Direction.x, 0, input.Direction.y);
|
|
248
|
+
transform.position += move * Runner.DeltaTime * 5f;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// State change callback
|
|
253
|
+
public static void OnHealthChanged(Changed<FusionPlayer> changed)
|
|
254
|
+
{
|
|
255
|
+
changed.Behaviour.OnHealthChangedLocal();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private void OnHealthChangedLocal()
|
|
259
|
+
{
|
|
260
|
+
// Update UI
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Input structure
|
|
265
|
+
public struct NetworkInputData : INetworkInput
|
|
266
|
+
{
|
|
267
|
+
public Vector2 Direction;
|
|
268
|
+
public NetworkButtons Buttons;
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Mirror (Open Source)
|
|
275
|
+
|
|
276
|
+
### Basic Setup
|
|
277
|
+
|
|
278
|
+
```csharp
|
|
279
|
+
// 1. Install Mirror from Asset Store (FREE)
|
|
280
|
+
|
|
281
|
+
public class MirrorPlayer : NetworkBehaviour
|
|
282
|
+
{
|
|
283
|
+
[SyncVar(hook = nameof(OnHealthChanged))]
|
|
284
|
+
public int health = 100;
|
|
285
|
+
|
|
286
|
+
[SyncVar]
|
|
287
|
+
public string playerName;
|
|
288
|
+
|
|
289
|
+
void Update()
|
|
290
|
+
{
|
|
291
|
+
if (!isLocalPlayer) return;
|
|
292
|
+
|
|
293
|
+
float h = Input.GetAxisRaw("Horizontal");
|
|
294
|
+
float v = Input.GetAxisRaw("Vertical");
|
|
295
|
+
|
|
296
|
+
if (h != 0 || v != 0)
|
|
297
|
+
{
|
|
298
|
+
CmdMove(new Vector3(h, 0, v));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
[Command] // Client → Server
|
|
303
|
+
void CmdMove(Vector3 direction)
|
|
304
|
+
{
|
|
305
|
+
transform.position += direction.normalized * 5f * Time.deltaTime;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
[Command]
|
|
309
|
+
void CmdShoot(Vector3 origin, Vector3 direction)
|
|
310
|
+
{
|
|
311
|
+
// Server validates and processes
|
|
312
|
+
if (Physics.Raycast(origin, direction, out RaycastHit hit, 100f))
|
|
313
|
+
{
|
|
314
|
+
if (hit.collider.TryGetComponent<MirrorPlayer>(out var target))
|
|
315
|
+
{
|
|
316
|
+
target.TakeDamage(25);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Tell all clients to show effect
|
|
321
|
+
RpcShowShootEffect(origin, direction);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
[ClientRpc] // Server → All Clients
|
|
325
|
+
void RpcShowShootEffect(Vector3 origin, Vector3 direction)
|
|
326
|
+
{
|
|
327
|
+
// Spawn tracer, play sound
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
[Server]
|
|
331
|
+
public void TakeDamage(int amount)
|
|
332
|
+
{
|
|
333
|
+
health -= amount;
|
|
334
|
+
if (health <= 0)
|
|
335
|
+
{
|
|
336
|
+
RpcDie();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
void OnHealthChanged(int oldValue, int newValue)
|
|
341
|
+
{
|
|
342
|
+
// Update health UI
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Lag Compensation
|
|
350
|
+
|
|
351
|
+
### Client-Side Prediction
|
|
352
|
+
|
|
353
|
+
```csharp
|
|
354
|
+
public class PredictedMovement : NetworkBehaviour
|
|
355
|
+
{
|
|
356
|
+
private Queue<InputFrame> _inputHistory = new();
|
|
357
|
+
private Vector3 _lastServerPosition;
|
|
358
|
+
|
|
359
|
+
struct InputFrame
|
|
360
|
+
{
|
|
361
|
+
public uint Tick;
|
|
362
|
+
public Vector3 Input;
|
|
363
|
+
public Vector3 PredictedPosition;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
void Update()
|
|
367
|
+
{
|
|
368
|
+
if (!IsOwner) return;
|
|
369
|
+
|
|
370
|
+
// 1. Capture input
|
|
371
|
+
Vector3 input = new Vector3(
|
|
372
|
+
Input.GetAxisRaw("Horizontal"),
|
|
373
|
+
0,
|
|
374
|
+
Input.GetAxisRaw("Vertical")
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// 2. Predict locally
|
|
378
|
+
Vector3 predictedPos = transform.position + input * moveSpeed * Time.deltaTime;
|
|
379
|
+
transform.position = predictedPos;
|
|
380
|
+
|
|
381
|
+
// 3. Store for reconciliation
|
|
382
|
+
_inputHistory.Enqueue(new InputFrame
|
|
383
|
+
{
|
|
384
|
+
Tick = NetworkManager.Singleton.LocalTime.Tick,
|
|
385
|
+
Input = input,
|
|
386
|
+
PredictedPosition = predictedPos
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// 4. Send to server
|
|
390
|
+
MoveServerRpc(input, NetworkManager.Singleton.LocalTime.Tick);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// When server confirms position
|
|
394
|
+
private void OnServerPositionReceived(Vector3 serverPos, uint serverTick)
|
|
395
|
+
{
|
|
396
|
+
_lastServerPosition = serverPos;
|
|
397
|
+
|
|
398
|
+
// Reconciliation: replay inputs since server tick
|
|
399
|
+
Vector3 reconciledPos = serverPos;
|
|
400
|
+
|
|
401
|
+
foreach (var frame in _inputHistory.Where(f => f.Tick > serverTick))
|
|
402
|
+
{
|
|
403
|
+
reconciledPos += frame.Input * moveSpeed * Time.fixedDeltaTime;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Check for misprediction
|
|
407
|
+
if (Vector3.Distance(reconciledPos, transform.position) > 0.01f)
|
|
408
|
+
{
|
|
409
|
+
// Correct position smoothly
|
|
410
|
+
transform.position = Vector3.Lerp(transform.position, reconciledPos, 0.5f);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Clean old history
|
|
414
|
+
while (_inputHistory.Count > 0 && _inputHistory.Peek().Tick <= serverTick)
|
|
415
|
+
{
|
|
416
|
+
_inputHistory.Dequeue();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Server-Side Rewind (Hit Detection)
|
|
423
|
+
|
|
424
|
+
```csharp
|
|
425
|
+
public class LagCompensatedCombat : NetworkBehaviour
|
|
426
|
+
{
|
|
427
|
+
// Store position history for rewind
|
|
428
|
+
private class PositionSnapshot
|
|
429
|
+
{
|
|
430
|
+
public float Timestamp;
|
|
431
|
+
public Vector3 Position;
|
|
432
|
+
public Quaternion Rotation;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private Queue<PositionSnapshot> _positionHistory = new();
|
|
436
|
+
private const int MAX_HISTORY = 64;
|
|
437
|
+
private const float MAX_REWIND_TIME = 0.5f; // 500ms max
|
|
438
|
+
|
|
439
|
+
void FixedUpdate()
|
|
440
|
+
{
|
|
441
|
+
if (IsServer)
|
|
442
|
+
{
|
|
443
|
+
// Record position history
|
|
444
|
+
_positionHistory.Enqueue(new PositionSnapshot
|
|
445
|
+
{
|
|
446
|
+
Timestamp = Time.time,
|
|
447
|
+
Position = transform.position,
|
|
448
|
+
Rotation = transform.rotation
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
while (_positionHistory.Count > MAX_HISTORY)
|
|
452
|
+
_positionHistory.Dequeue();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
[ServerRpc]
|
|
457
|
+
public void ShootServerRpc(Vector3 origin, Vector3 direction, float clientTimestamp)
|
|
458
|
+
{
|
|
459
|
+
// Calculate client's RTT
|
|
460
|
+
float rtt = NetworkManager.Singleton.NetworkConfig.NetworkTransport.GetCurrentRtt(OwnerClientId) / 1000f;
|
|
461
|
+
float rewindTime = Mathf.Min(rtt + 0.1f, MAX_REWIND_TIME);
|
|
462
|
+
|
|
463
|
+
// Rewind all players to client's perspective
|
|
464
|
+
var originalPositions = new Dictionary<NetworkObject, Vector3>();
|
|
465
|
+
|
|
466
|
+
foreach (var player in FindObjectsOfType<LagCompensatedCombat>())
|
|
467
|
+
{
|
|
468
|
+
if (player == this) continue;
|
|
469
|
+
|
|
470
|
+
originalPositions[player.NetworkObject] = player.transform.position;
|
|
471
|
+
player.RewindTo(Time.time - rewindTime);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Perform raycast in rewound state
|
|
475
|
+
if (Physics.Raycast(origin, direction, out RaycastHit hit, 100f))
|
|
476
|
+
{
|
|
477
|
+
if (hit.collider.TryGetComponent<LagCompensatedCombat>(out var target))
|
|
478
|
+
{
|
|
479
|
+
target.TakeDamage(25);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Restore positions
|
|
484
|
+
foreach (var kvp in originalPositions)
|
|
485
|
+
{
|
|
486
|
+
kvp.Key.transform.position = kvp.Value;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private void RewindTo(float targetTime)
|
|
491
|
+
{
|
|
492
|
+
var snapshot = _positionHistory
|
|
493
|
+
.Where(s => s.Timestamp <= targetTime)
|
|
494
|
+
.LastOrDefault();
|
|
495
|
+
|
|
496
|
+
if (snapshot != null)
|
|
497
|
+
{
|
|
498
|
+
transform.position = snapshot.Position;
|
|
499
|
+
transform.rotation = snapshot.Rotation;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## Matchmaking
|
|
508
|
+
|
|
509
|
+
### Simple Lobby System
|
|
510
|
+
|
|
511
|
+
```csharp
|
|
512
|
+
public class LobbyManager : NetworkBehaviour
|
|
513
|
+
{
|
|
514
|
+
public static LobbyManager Instance;
|
|
515
|
+
|
|
516
|
+
private NetworkList<LobbyPlayerData> _players;
|
|
517
|
+
|
|
518
|
+
public struct LobbyPlayerData : INetworkSerializable, IEquatable<LobbyPlayerData>
|
|
519
|
+
{
|
|
520
|
+
public ulong ClientId;
|
|
521
|
+
public FixedString32Bytes PlayerName;
|
|
522
|
+
public bool IsReady;
|
|
523
|
+
|
|
524
|
+
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
|
|
525
|
+
{
|
|
526
|
+
serializer.SerializeValue(ref ClientId);
|
|
527
|
+
serializer.SerializeValue(ref PlayerName);
|
|
528
|
+
serializer.SerializeValue(ref IsReady);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
public bool Equals(LobbyPlayerData other) => ClientId == other.ClientId;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private void Awake()
|
|
535
|
+
{
|
|
536
|
+
Instance = this;
|
|
537
|
+
_players = new NetworkList<LobbyPlayerData>();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
public override void OnNetworkSpawn()
|
|
541
|
+
{
|
|
542
|
+
if (IsServer)
|
|
543
|
+
{
|
|
544
|
+
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
|
|
545
|
+
NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnected;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
_players.OnListChanged += OnPlayersChanged;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
private void OnClientConnected(ulong clientId)
|
|
552
|
+
{
|
|
553
|
+
_players.Add(new LobbyPlayerData
|
|
554
|
+
{
|
|
555
|
+
ClientId = clientId,
|
|
556
|
+
PlayerName = $"Player {clientId}",
|
|
557
|
+
IsReady = false
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private void OnClientDisconnected(ulong clientId)
|
|
562
|
+
{
|
|
563
|
+
for (int i = 0; i < _players.Count; i++)
|
|
564
|
+
{
|
|
565
|
+
if (_players[i].ClientId == clientId)
|
|
566
|
+
{
|
|
567
|
+
_players.RemoveAt(i);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
[ServerRpc(RequireOwnership = false)]
|
|
574
|
+
public void SetReadyServerRpc(bool ready, ServerRpcParams rpcParams = default)
|
|
575
|
+
{
|
|
576
|
+
ulong clientId = rpcParams.Receive.SenderClientId;
|
|
577
|
+
|
|
578
|
+
for (int i = 0; i < _players.Count; i++)
|
|
579
|
+
{
|
|
580
|
+
if (_players[i].ClientId == clientId)
|
|
581
|
+
{
|
|
582
|
+
var player = _players[i];
|
|
583
|
+
player.IsReady = ready;
|
|
584
|
+
_players[i] = player;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
CheckAllReady();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private void CheckAllReady()
|
|
593
|
+
{
|
|
594
|
+
if (_players.Count >= 2 && _players.All(p => p.IsReady))
|
|
595
|
+
{
|
|
596
|
+
StartGame();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private void StartGame()
|
|
601
|
+
{
|
|
602
|
+
NetworkManager.Singleton.SceneManager.LoadScene("GameScene", LoadSceneMode.Single);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Anti-Cheat Patterns
|
|
610
|
+
|
|
611
|
+
### Server Authority
|
|
612
|
+
|
|
613
|
+
```csharp
|
|
614
|
+
public class ServerAuthorityExample : NetworkBehaviour
|
|
615
|
+
{
|
|
616
|
+
[ServerRpc]
|
|
617
|
+
private void AttackServerRpc(ulong targetId, ServerRpcParams rpcParams = default)
|
|
618
|
+
{
|
|
619
|
+
ulong attackerId = rpcParams.Receive.SenderClientId;
|
|
620
|
+
|
|
621
|
+
// 1. Validate attacker state
|
|
622
|
+
var attacker = NetworkManager.Singleton.SpawnManager.GetPlayerNetworkObject(attackerId);
|
|
623
|
+
if (!IsValidAttackState(attacker))
|
|
624
|
+
{
|
|
625
|
+
LogCheatAttempt(attackerId, "Invalid attack state");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 2. Validate target exists and is in range
|
|
630
|
+
var target = NetworkManager.Singleton.SpawnManager.SpawnedObjects[targetId];
|
|
631
|
+
if (target == null)
|
|
632
|
+
{
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
float distance = Vector3.Distance(attacker.transform.position, target.transform.position);
|
|
637
|
+
if (distance > maxAttackRange * 1.2f) // Allow small tolerance
|
|
638
|
+
{
|
|
639
|
+
LogCheatAttempt(attackerId, $"Attack range exceeded: {distance}");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// 3. Validate cooldown
|
|
644
|
+
if (!CanAttack(attackerId))
|
|
645
|
+
{
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 4. Apply damage
|
|
650
|
+
target.GetComponent<Health>().TakeDamage(attackDamage);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private void LogCheatAttempt(ulong clientId, string reason)
|
|
654
|
+
{
|
|
655
|
+
Debug.LogWarning($"[ANTI-CHEAT] Client {clientId}: {reason}");
|
|
656
|
+
|
|
657
|
+
// Increment strike counter
|
|
658
|
+
if (++_cheatStrikes[clientId] >= 3)
|
|
659
|
+
{
|
|
660
|
+
// Kick player
|
|
661
|
+
NetworkManager.Singleton.DisconnectClient(clientId);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
### Rate Limiting
|
|
668
|
+
|
|
669
|
+
```csharp
|
|
670
|
+
public class RateLimiter
|
|
671
|
+
{
|
|
672
|
+
private Dictionary<ulong, Queue<float>> _actionTimestamps = new();
|
|
673
|
+
|
|
674
|
+
public bool IsAllowed(ulong clientId, int maxActions, float timeWindow)
|
|
675
|
+
{
|
|
676
|
+
if (!_actionTimestamps.ContainsKey(clientId))
|
|
677
|
+
{
|
|
678
|
+
_actionTimestamps[clientId] = new Queue<float>();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
var timestamps = _actionTimestamps[clientId];
|
|
682
|
+
float now = Time.time;
|
|
683
|
+
|
|
684
|
+
// Remove old timestamps
|
|
685
|
+
while (timestamps.Count > 0 && now - timestamps.Peek() > timeWindow)
|
|
686
|
+
{
|
|
687
|
+
timestamps.Dequeue();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Check limit
|
|
691
|
+
if (timestamps.Count >= maxActions)
|
|
692
|
+
{
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
timestamps.Enqueue(now);
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Quick Reference
|
|
705
|
+
|
|
706
|
+
| Concept | Netcode | Photon Fusion | Mirror |
|
|
707
|
+
|---------|---------|---------------|--------|
|
|
708
|
+
| **Sync Variable** | NetworkVariable<T> | [Networked] | [SyncVar] |
|
|
709
|
+
| **Client → Server** | [ServerRpc] | RPC, Input | [Command] |
|
|
710
|
+
| **Server → Clients** | [ClientRpc] | RPC | [ClientRpc] |
|
|
711
|
+
| **Object Spawning** | NetworkObject.Spawn() | Runner.Spawn() | NetworkServer.Spawn() |
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Anti-Patterns
|
|
716
|
+
|
|
717
|
+
| ❌ Don't | ✅ Do |
|
|
718
|
+
|----------|-------|
|
|
719
|
+
| Trust client position | Server validates movement |
|
|
720
|
+
| Send full state every frame | Delta compression |
|
|
721
|
+
| No lag compensation | Implement prediction + reconciliation |
|
|
722
|
+
| Ignore bandwidth | Prioritize important data |
|
|
723
|
+
| Client-side hit detection | Server authoritative hits |
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
> **Remember:** The server is the source of truth. Clients lie.
|