Recent Changes - Search:

Main.SideBar (edit)

PmWiki

Upgrades

Introduction

Every UnitType and every Unit has certain properties, such as health, armor, shields, costs (minerals/oil) and so on. These properties can (currently) be modified through some kind of "TechCenter" using upgrades or technologies.
In Boson we made a difference between upgrades and technologies (do we still do so?), but in this document, I will call everything that changes the minimum/maximum values of a property a "upgrade". Therefore the term "upgrade" in this document does not refer to the upgrades as they are currently implemented in Boson (unless stated otherwise).

Most properties that are relevant to upgrades are numeric values (int, float/bofixed, ...). There are some exceptions (e.g. the list of unittypes a factory can produce), but I hope they are not so difficult to support, too. For now we consider numeric value only.

Every property has a minimum value, a maximum value and a current value. The minimum/maximum value is what most of this document is about.
We require some way to upgrade each of them:

  • Minimum value: a turret might have a "minimum shoot distance": it cannot shoot at targets closer than a certain distance. Upgrades may change this distance.
  • Maximum value: think of health. An upgrade may increase maximum value of health.
  • Current value: again think of health. An upgrade that changes the max health might also change update the health of all existing units to reflect that change. Note: changes to the current value are always one-shot changes. They are never permanent (max/min values are).
Note that in my notation "current value" means the value of a property in a Unit object. It does not mean the "value of the property after upgrades".

Requirements

Every property has a base value. This is stored in UnitProperties which reads it from the corresponding index.unit file. This base value should be modifyable (i.e. upgradeable) because of:

  • "Upgrades" or "Technologies" as implemented currently (i.e. things that can be researched). I will refer to these as "technologies" in the following.
    • Change the values +/- a certain value
    • Change the values +/- a certain percentage
    • Can apply to new units only, or to new units and all currently existing units
  • Experience
-> same as technologies, but applies to a single unit only
  • Motiviation? (e.g. fire power *= 0.001 when firing at civil units)
-> same as technologies, but applies to a single unit only
-> is always temporary only!
  • Moral
-> same as technologies, but applies to a single unit only
-> is temporary only!
  • Presence of a certain unit/unittype on the map (e.g. sightrange *= 2 when player has a radar)
-> same as technologies, but applies to a single unit only
-> is temporary only! (only as long as the other unit/unittype exists)
  • A certain unit/unittype is close to a unit (e.g. shields += 100 when unit is in range of a shield generator)
-> same as technologies, but applies to a single unit only
-> is temporary only! (only as long as the other unit/unittype is in range)
  • Special item. E.g. a unit can collect a "box" that causes an upgrade (e.g. "all units get repaired" or "all mobile units speed *= 2 for 2 minutes").
-> same as technologies, but can apply to all/some existing units and/or new units.
-> can be temporary
  • Certain events. E.g. "it's getting dark" implies "sight range /= 2".
essentially the same as "Special item"

As you can see most are very similar and we can group them together:

  • Upgrades
    • Change the values +/- a certain value
    • Change the values +/- a certain percentage
    • Can apply to new units only, to some existing units only, or to both
    • Can be permanent or temporary
      • Temporary: Might depend on another unit/unittype being on the map or in range

Implementation

From the above list items you may see that the current implementation is not able to support all possible upgrade types:

Current implementation

Currently we store the "base value" and the "max value" of an upgradeable property in UnitProperties. Note that "min value" is not stored currently and is usually context dependent (such as "min health is 0"). An upgrade always modifies the "max value" of the property, whereas the base value is the value as loaded from index.unit.
Therefore upgrades always apply to all units of that type. Upgrades that apply to

  • new units only
  • only some existing units of that type (e.g. "if in range of a shield generator")

are not possible. (note that "apply to some exising units is possible, but only as "one-shot" upgrade, i.e. that changes the current value only, not the maximum value)
Furthermore we do not store which upgrades modified a property. So if we have for example two upgrades that modify health, then it is not (not dependable at least) possible which one got applied to that property first. Therefore a property can not be un-done (e.g. upgrade A: health+=100, upgrade B: health*=2 -> the order of the upgrades makes a difference !). This means that

  • temporary upgrades

are not possible either.

Better implementation

In order to support the required features we have to change the implementation. One trivial and pretty obvious way (I use health as example, but other properties are analogous):

 
int Unit::health()
{
  float healthFactor = currentHealthFactor();
  int baseHealth = unitProperties()->baseHealth(); 
  int health = baseHealth;
  for (UpgradeIterator it = firstUpgrade; it != endUpgrade; ++it) {
    (*it)->applyUpgrade(&health);
  }
  return (int)(health * healthFactor);
}

As you can see, here all upgrades are stored in Unit (alternatively one could have two kinds of upgrades: one stored in Unit and one stored in UnitProperties. the example does not change from this).
The advantage is clear:

  • No features get lost: +/- values and +/- percentages is still possible.
  • Can apply to new units only, to exising units only or to both
note that "apply to existing units" can mean both: an actual upgrade object in the Unit class that modifies the max value (as shown in the example), or a change of the current value of the properties (in this case: the healthFactor) when the upgrade is gained.
  • Can apply to some of the existing units, instead of all
  • Upgrades can be temporary (can be removed at any time).

There is however a disadvantage (as usual with trivial implementations):

  • It is pain slow.

This is because every time health() is being called (this can be very often) we have to go through the whole loop.

An easy improvement: implement a cache:

 
int Unit::health()
{
  if (upgradesDirty) {
    mHealthCache = calculateHealthAsDescribedAbove();
  }
  return mHealthCache;
}

The main disadvantage here is that health() cannot be const, as mHealthCache needs to be modified. However by making mHealthCache mutable, this should be fixable, too.

Technical implementation problems

This section discusses (possible) problems with the approach discussed above.

Unit creation

Unit creation is rather simple: a Unit object is created as usual, with all upgradeable properties being "dirty", i.e. they are calculated as soon as they are accessed the very first time.
The only "difficult" part is to make sure that the Unit will get all upgrades that apply to new units. Most likely SpeciesTheme::loadNewUnit() will be responsible for this, which is always called for new units.

When a unit is created because a game is loaded (i.e. the Unit is created from XML), then loadNewUnit() is called as well (to initialize the values), so the upgrades must be cleared first, before loading them from XML. This is probably not a difficult issue.

I do not see any further issues for Unit creation.

IDs

We require some way to identify an upgrade. At runtime this is no problem, as we can simply use pointers to the upgrade objects. However when saving the game, we need a way to load it again.
Note that this problem does exist not only for savegames, but also for normal playfield files: we may one day support to create a map where certain technologies have already been researched!

I can see two ways to do this:

  • Save the upgrades to the XML file. This is the most simple solution. We can simply apply IDs dynamically to the Upgrades with no need for any user interaction. However this solution has one big disadvantage: When upgrades in the index.technologies (or any similar) file are modified, these modifications do not apply to the saved game.
  • Use a (user given) ID for every upgrade in the index.technologies (or any similar) file. This sounds much more simple than it is: we already have problems with these IDs in e.g. index.units files, as people tend to forget to modify the IDs when creating new units. With upgrades this could get worse, as we probably will have multiple files: upgrades.technologies, upgrades.moral, upgrades.experience, upgrades.special_items, ... and the IDs would have to be unique in all of the files. This sucks. It can be simplified slightly by using ID pairs: a (filename, ID) pair would be the actual internal ID. Still not the ideal solution.
    I would love to get rid of user-supplied IDs completely, but I dont see a good way:
    • Use the name to identify an upgrade. Not a good idea, as the name could change at any time (e.g. language corrections or re-wordings). Then the savegame file would be broken.
    • Use the (filename, index in file) pair as ID. Not good either, as the upgrades can get re-ordered.
    • Use the MD5 of the upgrade string. Not good either, as _any_ changes to the upgrade would change the ID then and therefore make the savegame files useless.

I have not found a satisfying solution to this problem.

Apply Upgrades

When the player researches a technology, the corresponding Player object is notified. It then stores the technology (that is the UpgradeProperties pointer) internally.

With the new design we require that such a technology is added to the upgrades list of all units that it applies to. Doing so should be rather simple, as the Player has access to all relevant units.

The same must happen with non-technology upgrades, but often is easier. For example an experience upgrade applies to a single unit only anyway, so adding it to that unit only (not to the Player object) is all natural and straight forward.
It may be a bit uncomfortable however for "global" upgrades, such as "Special items" or "Certain events" (e.g. "its getting dark"). Probably they will be added to the Player object, like we currently do with technologies, and then applied to all of its units.

Apply Upgrades to current values of existing units

This one is tricky. Let us use "health" as example. Image a unit has 50 out of 100 health and an upgrade "health += 20" is added to that unit.
There are several options now, how the current health value of that unit should look like after the upgrade got applied (the "maximum" value, is 120 afterwards, that is clear):

  1. 50 (no change to the current value)
  2. 70 (50 + 20)
  3. 60 ( 50 + (50/100)*20)

The first solution is the easiest, as nothing needs to be applied at all. However I think the other two solutions are preferred.
I would actually like to use the 2. solution, i.e. actually add 20 health to the current value. If the upgrade looks like "health += 20%", then I would like it to be 50 + 50*0.2.

I like the 3rd one more (although 2 is quite fine as well). Also, it might depend on what you upgrade. E.g. for shields and for weapon reloading times, I would use the first option - RL

This might be implementable using code like this:

 
void Unit::addUpgrade(upgrade)
{
 mUpgrades.append(upgrade);
 mUpgradesCacheDirty = true; // causes "MaxValue" and "MinValue" properties to be recalculated when they are accessed

 applyUpgradeToCurrentValues(upgrade);
}

applyUpgradeToCurrentValues() does not use a cache in the way MaxValue and /MinValue do, as current values are always "one shot" applies, the "current value" is never changed permanently (otherwise e.g. when changing health, the unit would be invincible).

The tricky part is "downgrading" a unit. This feature is required for temporary upgrades: imagine the unit collects an item "health += 100 for 2 minutes", then after two minutes the health must be reduced again. Why is this tricky? Imagine this

  • Unit has health 100 (out of 100)
  • Unit collects "health += 100 for 2 minutes" -> health = 200 for 2 minutes
  • Unit gets damaged to health=50 (out of 200)
  • 2 minutes are over "health += 100" is removed. But then unit would have health=-50 !

So the question is which of the following options do we choose now?

  1. reduce MaxHealth only, reduce current value only, if it exceeds the new MaxHealth (i.e. keep it at 50)
  2. reduce current health down to MinHealth. If MinHealth (i.e. 0) is reached, the unit is destroyed.
  3. keep healthFactor (i.e. health/maxHealth) constant.

I think the 2nd solution is correct here. After collecting the special item, the unit got damaged by 150 points, if it did not have the special item, it would already be destroyed now. So it is logical that it'll be destroyed ony it loses that item.

I'm not sure about this, it might be confusing when your unit suddenly blows up when the battle is over. I think I'd rather go for the 3rd option - RL
AB: agreed.
Update:

Currently we do not apply any values manuelly when an upgrade is gained. Instead we use a "factor" for the "currentvalue", i.e. currentValue = factor * maxValue, where maxValue is the value that gets upgraded.
Therefore we follow the 3rd option from above: we keep heatlh/maxHealth factor constant.

Status

(Note: In the article above the term "current implementation" refers to the old implementation before 2005/07/07)

The architecture described in this article has been implemented. Today (2005/07/10) we store a "base" value of every upgradeable property in the classes UnitProperties or PluginProperties. Together with all upgrades, such a base value makes up the real "current" value, which is calculated on the fly and recalculated whenever the upgrades change (the "base" value is read once on startup from the config files only and does not change afterwards).

A list of upgrades is maintained by every object of the classes

  • UnitProperties - upgrades that apply to all new (not the currently existing!) units of this type. Also upgrades of e.g. mineralCost or oilCost, which do not make sense after the unit got produced.
  • Unit - upgrades of a specific unit. This contains upgrades that apply to all units of that type as well as upgrades that are special to this unit only. When a new unit is being produced, it gains all upgrades of the corresponding UnitProperties object.
  • PluginProperties - like UnitProperties, but a unit type may have none or several objects of this class. Any object may have upgrades. Weapon properties are implemented through this class, too.
  • UnitPlugins - like Unit, but a unit may have none or several objects of this class. Weapons are implemented through this class, too.

Note that all classes automatically (without any additional API requirements) temporary upgrades.
The API to add an upgrade is relatively simple: object->addUpgrade(upgradePointer); or object->removeUpgrade(upgradePointer);
These calls modify the upgrades list only, they do NOT touch the properties. The properties are updated once they are accessed, as described in this article.

Edit - History - Print - Recent Changes - Search
Page last modified on July 10, 2005, at 20:25