f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
228 lines
7.3 KiB
Markdown
228 lines
7.3 KiB
Markdown
---
|
|
id: wiki-2026-0508-control-points
|
|
title: Control Points
|
|
category: 10_Wiki/Topics
|
|
status: verified
|
|
canonical_id: self
|
|
aliases: [Capture Points, KOTH, Domination]
|
|
duplicate_of: none
|
|
source_trust_level: A
|
|
confidence_score: 0.85
|
|
verification_status: applied
|
|
tags: [game-design, multiplayer, objectives, fps]
|
|
raw_sources: []
|
|
last_reinforced: 2026-05-10
|
|
github_commit: pending
|
|
tech_stack:
|
|
language: csharp-gdscript
|
|
framework: unity-godot-unreal
|
|
---
|
|
|
|
# Control Points
|
|
|
|
## 매 한 줄
|
|
> **"매 spatial objective — 매 team 이 매 zone 의 occupy 통해 score/win."**. Control Points는 multiplayer game 의 가장 ubiquitous objective primitive. Domination, KOTH, Capture-and-Hold, Push, Hardpoint 모두 변형. Team Fortress 2, Battlefield, Overwatch, Apex, CS Bombsite (variant), Splatoon (area-based) 의 핵심. 2026년 server-authoritative netcode + lag compensation 패턴 매 stable.
|
|
|
|
## 매 핵심
|
|
|
|
### 매 variants
|
|
| Variant | Mechanic |
|
|
|---|---|
|
|
| **Domination** | Multiple points, hold majority for tickets/score |
|
|
| **KOTH** | Single point, hold-time wins |
|
|
| **Capture & Hold** | Capture sequentially, last team standing |
|
|
| **Hardpoint** | Single rotating point, score over time |
|
|
| **Push/Payload** | Mobile control point along track |
|
|
| **Linear (5CP TF2)** | Sequential capture, central pivot |
|
|
|
|
### 매 mechanics
|
|
- **Capture progress**: 0~100%, increases with attackers in zone.
|
|
- **Multi-capture rate**: more attackers → faster (capped, e.g. 2x at 2+).
|
|
- **Contest**: defenders inside → progress paused.
|
|
- **Lock/unlock**: previous point capture unlocks next.
|
|
- **Decay**: progress drops when zone empty (configurable).
|
|
- **Overtime**: contested point prevents game end.
|
|
|
|
### 매 design principles
|
|
- **Sightline balance**: defenders 의 advantage 와 attacker chokes 의 균형.
|
|
- **Capture time**: too short → trivial, too long → stalemate. 5-15s typical.
|
|
- **Cap-zone size**: encourages clustering vs spread.
|
|
- **Spawn distance**: defender respawn 가 너무 가까우면 attack 불가.
|
|
|
|
### 매 응용
|
|
1. FPS multiplayer modes (Overwatch, BF, CoD).
|
|
2. MOBA jungle camps / objectives (Roshan, Drake areas).
|
|
3. RTS resource nodes (StarCraft expansions).
|
|
4. MMO PvP zones (WoW battlegrounds).
|
|
|
|
## 💻 패턴
|
|
|
|
### Unity C# — control point trigger
|
|
```csharp
|
|
using UnityEngine;
|
|
using Unity.Netcode;
|
|
|
|
public class ControlPoint : NetworkBehaviour {
|
|
public NetworkVariable<float> Progress = new(0f);
|
|
public NetworkVariable<int> OwnerTeam = new(-1);
|
|
|
|
[SerializeField] private float captureRate = 10f; // pct/sec per attacker
|
|
[SerializeField] private float maxRate = 20f;
|
|
|
|
private readonly Dictionary<int, int> teamCount = new();
|
|
|
|
private void OnTriggerEnter(Collider other) {
|
|
if (!IsServer) return;
|
|
if (other.TryGetComponent<Player>(out var p)) {
|
|
teamCount.TryGetValue(p.Team, out var n);
|
|
teamCount[p.Team] = n + 1;
|
|
}
|
|
}
|
|
|
|
private void OnTriggerExit(Collider other) {
|
|
if (!IsServer) return;
|
|
if (other.TryGetComponent<Player>(out var p)
|
|
&& teamCount.TryGetValue(p.Team, out var n)) {
|
|
teamCount[p.Team] = Mathf.Max(0, n - 1);
|
|
}
|
|
}
|
|
|
|
private void FixedUpdate() {
|
|
if (!IsServer) return;
|
|
|
|
var teams = new List<KeyValuePair<int,int>>(teamCount);
|
|
teams.Sort((a, b) => b.Value.CompareTo(a.Value));
|
|
|
|
if (teams.Count == 0 || teams[0].Value == 0) return;
|
|
|
|
// Contested: top two teams equal & nonzero
|
|
if (teams.Count > 1 && teams[0].Value == teams[1].Value) return;
|
|
|
|
var attacker = teams[0];
|
|
var rate = Mathf.Min(maxRate, captureRate * Mathf.Sqrt(attacker.Value));
|
|
|
|
if (OwnerTeam.Value == attacker.Key) {
|
|
Progress.Value = Mathf.Min(100f, Progress.Value + rate * Time.fixedDeltaTime);
|
|
} else {
|
|
Progress.Value = Mathf.Max(0f, Progress.Value - rate * Time.fixedDeltaTime);
|
|
if (Progress.Value <= 0f) OwnerTeam.Value = attacker.Key;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Godot 4 / GDScript
|
|
```gdscript
|
|
extends Area3D
|
|
class_name ControlPoint
|
|
|
|
@export var capture_rate: float = 10.0
|
|
var progress: float = 0.0
|
|
var owner_team: int = -1
|
|
var team_in_zone: Dictionary = {}
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if not multiplayer.is_server(): return
|
|
|
|
var top_team := -1
|
|
var top_count := 0
|
|
var contested := false
|
|
|
|
for team in team_in_zone:
|
|
var n: int = team_in_zone[team]
|
|
if n > top_count:
|
|
top_count = n
|
|
top_team = team
|
|
contested = false
|
|
elif n == top_count and n > 0:
|
|
contested = true
|
|
|
|
if contested or top_count == 0: return
|
|
|
|
var rate = capture_rate * sqrt(top_count)
|
|
if owner_team == top_team:
|
|
progress = min(100.0, progress + rate * delta)
|
|
else:
|
|
progress = max(0.0, progress - rate * delta)
|
|
if progress <= 0.0:
|
|
owner_team = top_team
|
|
_notify_capture(top_team)
|
|
```
|
|
|
|
### Server-authoritative state with lag compensation
|
|
```csharp
|
|
// Server stores state snapshots for last 1s
|
|
private readonly CircularBuffer<Snapshot> history = new(60);
|
|
|
|
public bool WasInsideAtTime(Vector3 playerPos, float clientTime) {
|
|
var snap = history.SampleAt(clientTime);
|
|
return snap.Bounds.Contains(playerPos);
|
|
}
|
|
```
|
|
|
|
### Configurable progression (data-driven)
|
|
```json
|
|
{
|
|
"id": "cp_central",
|
|
"captureTimeSeconds": 8,
|
|
"multiCapMultiplier": [1.0, 1.5, 1.75, 2.0],
|
|
"decayRate": 0.5,
|
|
"unlockedBy": ["cp_a", "cp_b"],
|
|
"scoresPerSecond": 10
|
|
}
|
|
```
|
|
|
|
### UI broadcast (client-side prediction visual)
|
|
```typescript
|
|
// Client receives Progress NetworkVariable changes,
|
|
// interpolates between ticks for smooth bar fill
|
|
useEffect(() => {
|
|
const start = performance.now();
|
|
const startVal = displayedProgress;
|
|
const targetVal = serverProgress;
|
|
|
|
const tick = () => {
|
|
const t = Math.min(1, (performance.now() - start) / 100);
|
|
setDisplayedProgress(startVal + (targetVal - startVal) * t);
|
|
if (t < 1) requestAnimationFrame(tick);
|
|
};
|
|
tick();
|
|
}, [serverProgress]);
|
|
```
|
|
|
|
## 매 결정 기준
|
|
| 상황 | Approach |
|
|
|---|---|
|
|
| Casual fast-match | KOTH, single-point, 3-5 min rounds |
|
|
| Competitive | Linear/5CP, longer rounds, overtime |
|
|
| Asymmetric | Push/Payload (attack vs defend) |
|
|
| Objective rotation | Hardpoint (rotating zone keeps action moving) |
|
|
| Large maps | Domination (multiple distributed) |
|
|
|
|
**기본값**: server-authoritative + 5-10s capture + multi-cap multiplier + decay + contested-pause + overtime.
|
|
|
|
## 🔗 Graph
|
|
- 변형: [[Domination]]
|
|
- Adjacent: [[Lag Compensation]]
|
|
|
|
## 🤖 LLM 활용
|
|
**언제**: multiplayer mode design, level layout review, balance tuning, netcode design for objectives.
|
|
**언제 X**: single-player, asynchronous (turn-based), pure deathmatch (no objective).
|
|
|
|
## ❌ 안티패턴
|
|
- **Client-authoritative capture**: trivially exploitable — server-side only.
|
|
- **Spawn too close to objective**: defender immortal — distance + lockout window.
|
|
- **No contested-pause**: solo defender can't stall — feels unfair.
|
|
- **Capture too short**: zerg wins, no skill — 8-12s standard.
|
|
- **No decay**: half-cap then leave is permanent — partial progress decay.
|
|
|
|
## 🧪 검증 / 중복
|
|
- Verified (Valve TF2 design / Blizzard Overwatch dev blogs / GDC talks 2018-2024).
|
|
- 신뢰도 A-.
|
|
|
|
## 🕓 Changelog
|
|
| 날짜 | 변경 |
|
|
|---|---|
|
|
| 2026-05-08 | Phase 1 |
|
|
| 2026-05-10 | Manual cleanup — variants + Unity/Godot impl + netcode |
|