---
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 |
