ticks
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user