This commit is contained in:
2026-11-10 20:31:33 +01:00
parent 2ef8e857e0
commit 8c24b112d4
20 changed files with 83 additions and 66 deletions
+27 -3
View File
@@ -104,6 +104,30 @@ To implement the RTS Civilization game, develop modular subsystems decoupled fro
+-----------------+
```
### 3.1 Simulation Time & Tick System
The game combines continuous real-time rendering and interpolation (`deltaTime`) with a configurable **Simulation Tick** (`movement_tick_duration` in `config/game.yaml`, defaulting to `30.0` seconds at $1\times$ simulation speed):
1. **Tick Timer & Speed Multiplier:**
- `GameManager::update` advances an internal tick timer by `deltaTime * simSpeedMultiplier`.
- When the timer reaches `movement_tick_duration`, a simulation tick fires (`processTickYields`).
2. **Discrete Per-Tick Economy & Progression (`processTickYields`):**
- **Food & Population:** City food yield is converted into inhabitants (`food * foodToInhabitantsRatio`).
- **Labor (Production):** City labor yield is added to the active construction project (`currentLabor`), completing units or buildings when `laborCost` is reached, or accumulating locally if idle.
- **National Stockpiles:** Culture, Faith, and Gold yields from all owned cities are added to the player's national treasury.
- **Science & Research:** Total science generated per tick across a player's cities is applied directly to the active technology in the player's `researchQueue`.
3. **Continuous Real-Time Unit Movement (`movement_speed`, Scaled by Tick Duration):**
- Units move smoothly every frame, and their movement rate is defined in **movement points (base hexes) per tick** (`movement_speed` in `config/units/*.yaml`, stored in `UnitConfig::movementSpeed` — higher values mean faster movement, e.g., `2.0` for infantry/civilian, `3.0` for galley, `4.0` for cavalry).
- Effective speed in hexes per second is calculated dynamically per hex segment as:
$$\text{hexesPerSec} = \frac{\text{movementSpeed}}{\text{tickDuration} \times \text{stepCost}} \times \text{simSpeedMultiplier}$$
- On flat terrain (`stepCost = 1.0`), a unit with `movementSpeed = 2.0` traverses $2.0$ hexes during one full simulation tick ($0.5$ ticks per hex).
4. **Unit Construction & Action Durations (`action_durations`):**
- Unlike `movement_speed` (which is a rate per tick), unit build actions (such as `boerderij_bouwen` and `build_road` under `action_durations` in `config/units/builder.yaml`) are configured as **durations in simulation ticks** (ticks per action, accessed via `UnitConfig::getActionDuration`).
- Total real-time build duration in seconds is calculated as:
$$\text{duration} = \text{ticks} \times \text{movement\_tick\_duration}$$
- While building, `buildProgress` interpolates continuously from $0.0$ to $1.0$ over `duration` seconds (scaled by `simSpeedMultiplier`).
- Similarly, city production (`labor_cost` in `config/units/*.yaml` and `config/buildings.yaml`) and technology research (`science_cost` in `config/technologies.yaml`) progress discretely each tick based on the city/empire's per-tick yield.
---
## 4. Agents & Unit Specifications
@@ -124,7 +148,7 @@ Units are autonomous or player-directed entities traversing the hex map in real
- **Siege:** Catapult $\rightarrow$ Trebuchet $\rightarrow$ Bombard (high bonus damage against city walls).
- **Naval:** Galley $\rightarrow$ Caravel $\rightarrow$ Frigate $\rightarrow$ Battleship (operates only on water hexes).
### 4.2 Unit Data Model (`Unit.h`)
### 4.2 Unit Data Model (`unit.h` / `unitconfig.h`)
```cpp
enum class UnitType { Settler, Worker, Warrior, Archer, Horseman, Galley };
enum class UnitState { Idle, Moving, Attacking, Harvesting, Building, Fortified };
@@ -146,14 +170,14 @@ public:
float health, maxHealth;
float attackDamage;
float defense;
float movementSpeed; // hexes per second
float movementSpeed; // movement points (base hexes) per simulation tick
float visionRange; // in hex radius
// Path & Target
std::vector<std::pair<int, int>> currentPath;
int targetEntityId; // target unit or city
void update(float deltaTime);
void update(float deltaTime, float tickDuration, float simSpeedMultiplier);
void commandMoveTo(int destX, int destY);
void commandAttack(int targetId);
void commandBuildImprovement(int improvementType);
+1 -1
View File
@@ -53,7 +53,7 @@ ui:
action_scheduled_after_current: "%1 scheduled after current action"
action_cancelled: "%1 cancelled"
farm_queued: "Build Farm queued"
farm_started: "Constructing Farm..."
farm_started: "Constructing Farm... (%1 ticks)"
farm_completed: "Farm completed!"
farm_completed_consumed: "Farm completed! (Builder consumed)"
farm_completed_charges: "Farm completed! (%1 %2 remaining)"
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 2
required_tech: "archery"
obsolete_tech: "machinery"
icon: images/units/slinger.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+3 -4
View File
@@ -16,8 +16,7 @@ actions:
- boerderij_bouwen
- build_road
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
action_speeds:
boerderij_bouwen: 10.0
build_road: 2
action_durations:
boerderij_bouwen: 2.0
build_road: 2.0
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 2
required_tech: "machinery"
obsolete_tech: ""
icon: images/units/slinger.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 1
required_tech: "sailing"
obsolete_tech: ""
icon: images/units/galley.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 1
required_tech: "horseback_riding"
obsolete_tech: "stirrups"
icon: images/units/warrior.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 1
required_tech: "stirrups"
obsolete_tech: ""
icon: images/units/warrior.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 1
required_tech: "gunpowder"
obsolete_tech: ""
icon: images/units/warrior.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -9,9 +9,8 @@ labor_cost: 2000
required_tech: ""
obsolete_tech: ""
icon: images/units/settler.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- steden_bouwen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 2
required_tech: ""
obsolete_tech: "archery"
icon: images/units/slinger.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 1
required_tech: "bronze_working"
obsolete_tech: "gunpowder"
icon: images/units/warrior.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 1
required_tech: "iron_working"
obsolete_tech: "gunpowder"
icon: images/units/warrior.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+2 -3
View File
@@ -11,9 +11,8 @@ attack_range: 1
required_tech: ""
obsolete_tech: "iron_working"
icon: images/units/warrior.png
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
recharge_ticks: 2
attack_flash_ticks: 1
actions:
- verplaatsen
- aanvallen
+6 -5
View File
@@ -1265,9 +1265,10 @@ void GameManager::continueCurrentUnitAction(Unit &unit, Matrix2D<TerrainPoint> *
}
// Unit is on the target tile: start constructing the improvement
float duration = unit.config.getActionDuration("boerderij_bouwen", 10.0f);
float ticks = unit.config.getActionDuration("boerderij_bouwen", 2.0f);
float duration = ticks * m_movementTickDuration;
unit.startBuilding(impId, duration);
addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."),
addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm... (%1 ticks)").arg(ticks, 0, 'g', 3),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
break;
}
@@ -1789,10 +1790,10 @@ void GameManager::advanceRoadRoute(Unit &unit, Matrix2D<TerrainPoint> *mapPoints
QString defaultType = m_roadConfigManager.defaultRoadType();
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType);
QString roadName = rCfg ? rCfg->name : "Road";
int ticks = m_roadConfigManager.buildDurationTicks();
float duration = (float)ticks * m_movementTickDuration;
float ticks = unit.config.getActionDuration("build_road", (float)m_roadConfigManager.buildDurationTicks());
float duration = ticks * m_movementTickDuration;
unit.startBuilding(QString("road:%1").arg(defaultType), duration);
addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks),
addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks, 0, 'g', 3),
unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
return;
}
+2 -2
View File
@@ -770,8 +770,8 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
}
// Recharge bar below military units when recharging
if (unit->config.canAttack() && unit->isRecharging()) {
float rechargePercent = std::max(0.0f, std::min(1.0f, unit->rechargeTimer / unit->config.rechargeTime));
if (unit->isRecharging()) {
float rechargePercent = std::max(0.0f, std::min(1.0f, unit->rechargeTimer / unit->config.rechargeTicks));
QRectF recBg(headCenter.x() - 14.0f, headCenter.y() - R - 5.5f, 28.0f, 3.5f);
painter.setBrush(QColor(30, 30, 30, 220));
painter.setPen(Qt::NoPen);
+9 -6
View File
@@ -12,7 +12,8 @@ Unit::Unit(int uId, int pId, const UnitConfig &cfg, int startX, int startY, floa
buildCharges(cfg.buildCharges),
state(UnitState::Idle), pathIndex(0), moveProgress(0.0f),
targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f), rechargeTimer(0.0f),
buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId("")
buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId(""),
movement_tick_duration(30.0f) // Default: 30 seconds per tick at 1x speed
{
QVector2D w = Pathfinder::hexToWorld(startX, startY);
worldPos = QVector3D(w.x(), w.y(), groundZ);
@@ -43,8 +44,8 @@ void Unit::performAttack(Unit &target)
{
if (!canAttack(target)) return;
attackFlashTimer = config.attackFlashDuration / 1000.0f; // Convert ms to seconds
rechargeTimer = config.rechargeTime / 1000.0f; // Convert ms to seconds
attackFlashTimer = config.attackFlashTicks * movement_tick_duration; // Convert ticks to seconds
rechargeTimer = config.rechargeTicks * movement_tick_duration; // Convert ticks to seconds
target.takeDamage((float)config.attackDamage);
@@ -60,8 +61,8 @@ void Unit::performAttack(Unit &target)
void Unit::takeDamage(float damage)
{
health = std::max(0.0f, health - damage);
attackFlashTimer = 0.4f;
rechargeTimer = config.rechargeTime / 1000.0f; // Reset recharge on taking damage
attackFlashTimer = movement_tick_duration; // Flash lasts 1 tick
rechargeTimer = config.rechargeTicks * movement_tick_duration; // Reset recharge to full duration
}
float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
@@ -247,8 +248,10 @@ void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
}
// Update recharge timer (cooldown between attacks)
// Recharge decays based on simulation ticks, not real-time multiplier
if (rechargeTimer > 0.0f) {
rechargeTimer = std::max(0.0f, rechargeTimer - deltaTime * simSpeedMultiplier);
float tickDurationSec = deltaTime / movement_tick_duration;
rechargeTimer = std::max(0.0f, rechargeTimer - tickDurationSec);
}
if (state == UnitState::Building) {
+3
View File
@@ -56,6 +56,9 @@ public:
float health;
float maxHealth;
int buildCharges;
// Simulation parameters (set during unit creation or via GameManager)
float movement_tick_duration; // Seconds per simulation tick
UnitState state;
// Movement Path
+2 -2
View File
@@ -82,11 +82,11 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
if (root.hasChild(key)) {
const YamlNode &sNode = root[key];
for (auto it = sNode.children.begin(); it != sNode.children.end(); ++it) {
cfg.actionSpeeds[it.key()] = it.value().asFloat(10.0f);
cfg.actionSpeeds[it.key()] = it.value().asFloat(2.0f);
}
}
};
parseActionMap("action_speeds");
parseActionMap("action_durations");
parseActionMap("action_durations");
m_configs[cfg.id] = cfg;
+8 -10
View File
@@ -48,23 +48,21 @@ struct UnitConfig {
QString obsoleteTech;
QString iconPath;
QStringList actions;
QMap<QString, float> actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f)
QMap<QString, float> actionSpeeds; // Duration in simulation ticks per action (e.g. "boerderij_bouwen": 2.0f)
QStringList impassableTerrains;
QStringList passableTerrains;
// Recharge / Cooldown configuration for military units
int rechargeTime; // Time in milliseconds between attacks
float rechargeDuration; // Duration of recharge state (optional, for visual indication)
int attackFlashDuration; // How long the flash effect lasts (in ms)
// Recharge / Cooldown configuration for military units (in simulation ticks)
int rechargeTicks; // Ticks between attacks for military units
int attackFlashTicks; // Ticks for visual attack flash effect
UnitConfig()
: id(""), name(""), category(UnitCategory::Builder),
movementDomain(MovementDomain::Land), health(100),
movementSpeed(2.0f), cost(10), laborCost(1000), attackDamage(0), attackRange(1),
buildCharges(-1), infiniteCharges(true), requiredTech(""), obsoleteTech(""), iconPath(""),
rechargeTime(500), // Default recharge time: 500ms (0.5 seconds)
rechargeDuration(300), // Visual cooldown bar duration: 300ms
attackFlashDuration(400) // Attack flash effect duration: 400ms
rechargeTicks(2), // Default recharge: 2 simulation ticks
attackFlashTicks(1) // Attack flash effect: 1 tick
{}
bool isRanged() const { return attackRange > 1; }
@@ -77,11 +75,11 @@ struct UnitConfig {
bool canBuildRoad() const { return actions.contains("build_road") || actions.contains("weg_bouwen"); }
bool isBuilder() const { return canBuildFarm() || canBuildRoad(); }
float getActionDuration(const QString &actionName, float fallback = 10.0f) const {
float getActionDuration(const QString &actionName, float fallbackTicks = 2.0f) const {
if (actionSpeeds.contains(actionName)) {
return actionSpeeds.value(actionName);
}
return fallback;
return fallbackTicks;
}
};