Hello,

[English is not my native language, please forgive any mistake I made.]

In three weeks from now my exams will be over, and I will have a full week
free to hack on Freeciv. As 3.0 is in the works now, I guess it's the best
timing to add big features. As per [1], a few things are already planned.

I'd like to share my troughs on features that could be implemented. Below is
an essay on a new, more modular drawing system. If you read it, sit
comfortably and get some coffee before you begin: it is rather long.

Happy reading,
Louis94

[The following is best viewed in some monospace font.]

Making the drawing system more modular
======================================

  Motivation
  ----------

Currently, the drawing system accepts 22 different layer types, hardcoded
in tilespec.h. All of them are handled differently, resulting in a big
switch statement in fill_sprite_array(). The following layers are drawn, in
the order they are given:
  - Background: a single, flat color
  - Terrain 1-2-3: terrain, with several match/tile types
  - Water: "River" extras
  - Roads: "Road*Combined" extras (three different styles)
  - Specials 1: "Single1" extras and first layer of "3Layer" extras
  - Grid 1: grid for isometric tilesets
  - City 1: city, except city bar or name
  - Specials 2: "Single2" extras and second layer of "3Layer" extras
  - Fog: fog of war (if not drawn together with darkness)
  - Units: units, having focus or not
  - Special 3: third layer of "3Layer" extras, and base flags. Bases may not
    use "3Layer"
  - City 2: city size
  - Grid 2: grid for non-iso tilesets
  - Overlays: city overlays (production, tiles being worked by other cities
    in city map)
  - Tile label
  - City bar
  - Focus units: same as Unit, but for focus unit
  - Goto: goto line
  - Worker task: unit activity (mine, irrigate, road, transform)
  - Editor: editor selection and user attention
In addition to that, darkness (unknown tiles) has to be drawn on top of one
of the three terrain layers. Darkness supports five tile types, similar to
how terrains are drawn.
 The current drawing system also has inconsistencies (see Gna! ticket #5580).
 I think the above could be made simpler and much more flexible. This would
require changes to a few files currently not related to drawing. So here is
a design doing just that.
 The reasoning is simple: Many of the layers above are sprites (images) drawn
on tiles selected by some rule. So let's say a "sprite layer" is a
combination of sprites and rules describing where to draw them. And let's
allow tilesets to define their own layers.
 Freeciv already has a way to describe rules, called requirements. Ruleset
developers know them already. Generalizing requirements to fit the needs the
design described below would be straightforward [a].

  Architecture
  ------------

In this section, I describe the architecture of a map drawing system
addressing the above concerns. This is not from a coder point of view, but I
think the implementation is possible (and not too difficult).

 # Matches

The basic decision element is called a "match" [b]. A match is a list of
requirements to be met. The reason for defining matches is that the existing
framework doesn't support the logical OR operation. It is not possible to say
"req 1 OR req 2". The definition of a match could look like this:

; This will match any Oceanic tile except lakes.
[match_sea_not_lake]
reqs =
  { "type",         "name",    "range"
    "TerrainClass", "Oceanic", "Local"
  }
nreqs =
  { "type",    "name", "range"
    "Terrain", "lake", "Local"
  }

When the system looks for a match, it will first search user-defined
ones. If nothing is found, an attempt will be made to create a match of the
following form:

; These will be automatically created for you.
[match_<terrain>]
reqs =
  { "type",    "name",      "range"
    "Terrain", "<terrain>", "Local"
  }

If no candidate is found, the system will try mine, then irrigation,
farmland, specials, roads (incl. rivers), bases, units and city styles, then
"fogged", "unknown" and "known". In last resort, an warning will be
displayed and a match with no requirement will be generated (such a
match is always true).

 # Drawing tiles

A map is an array of tiles. To distinguish the drawing definitions from the
tiles themselves, the former are called "drawing tiles", or d-tiles. A d-tile
is declared this way:

[tile_landwater]
center_x = 46 ; These two numbers define where the center of the tile is on
center_y = 24 ; the sprite. If not set, the sprite will be drawn centered.
sprites = "t.l0.lake#" ; This is a list of names of sprites (images) to draw.
                       ; One of them will be randomly chosen by the system.
                       ; If the last character is #, it will be replaced by
                       ; numbers starting from 0. If the 0th sprite doesn't
                       ; exist, the system will make a last try with the
                       ; sharp removed.
                       ; The system will append suffixes when drawing layers
                       ; with "Edges" or "Adjacent" match types; see below.
matches = "lake", "pond" ; The sprite will be drawn on tiles matching the
                         ; requirements of [match_lake] OR [match_pond].
nearby  =                ; This drawing tile will interact with nearby tiles
  { "name",         "id" ; matching [sea_not_lake]. The details depend on the
    "sea_not_lake", "1"  ; layer this d-tile is used in.
  }                      ; There may be several matches here. Each one may
                         ; define a single-letter identifier to be used in
                         ; suffixes.
                         ; For backwards compatibility, the default id is "1"
                         ; if there is only one match, and the first letter
                         ; of the name otherwise.

IMPORTANT NOTE:
Having several d-tiles in a layer matching the same tile results in undefined
behavior [b].

 # Layers

Layers are the glue holding d-tiles together. They define how they interact
with their neighbors (those matching one of the "nearby" matches).
Each layer has a name and a position in the layer stack. When drawing a map,
the system will begin by drawing all d-tiles of the deepest layer, then go
upper in the stack until it reaches the top.

The list of all layers is given in the [tilespec] section:

[tilespec]
layers = "terrain",     ; This is the deepest layer (drawn first)
         "water",
         "irrigation",
         "roads",
         "bases",
         "specials",
         "cities",
         "fog",
         "units",
         "focus",
         "active_units" ; This will be the last layer drawn

For each item in the list, there must be a corresponding [layer_<name>]
section. It must have at least three fields: "match", "type" and "tiles".
  - The "type" field is used to describe what the layer draws. This is used
    for hiding/showing layers (trough the "View" menu).
     Some layer types are not handled by the layer system; they are marked by
    an exclamation mark (!). Each of these layer types may only be present
    once in a given tileset, and no other field is allowed. One can expect
    them to be placed consistently if not defined explicitly.
      * "Terrain", "Roads", "Irrigation", "Bases", "Mines", "Cities",
        "Resources", "Pollution"
      * "Units", "ActiveUnits"
      * "Fog": Fog of war and unknown tiles
      * (!) "SimpleFog": Automatic fogging. Drawn above all layers of the
                         first bullet.
      * (!) "Overlays": Inaccessible and unworkable (in city dialog) tiles.
                        If not present, drawn above all layers of the first
                        bullet, but below "Autofog".
      * (!) "Borders",
            "Grid": If not present, drawn above the top "Terrain" layer.
      * (!) "CityFlags": If not present, drawn below the first "Cities"
                         layer.
      * (!) "UnitFlags": If not present, drawn below the first "Units" and
                         "ActiveUnits" layers.
      * (!) "BaseFlags": If not present, drawn below the first "Bases" layer.
      * (!) "CityInfo": City bar, or city production/size/name if disabled.
                        If not present, drawn below the first "ActiveUnits"
                        layer.
      * (!) "WorkedTiles": Tiles being worked by cities. If not present,
                           drawn above "Grid".
      * (!) "TileLabels": If not present, drawn below "CityInfo".
      * (!) "TradeRoutes",
            "Editor",
            "GotoLine": If not present, drawn above everything else in the
                        order given here.
  - The "tiles" field is a list of d-tiles to draw on this layer.
  - The "match" field sets how sprites will be matched to their neighbors. It
    can take one of the following values:
      * "None": No matching is done; the base sprite will always be drawn. No
                suffix is appended to the sprite name.
      * "Edges": Matching is done along edges of the tile. A suffix is added
                 to the sprite name.
      * "Edges": Matching is done at corners of the tile. A suffix is added
                 to the sprite name.
      * "Full": Matching is done among all adjacent tiles (ie, both along
                edges and at corners).
  - The "sprite" field describes how sprites are chosen. It can take one of
    the following values:
      * "Whole": There is a sprite representing the full tile for any of the
                 possible match states. This is the best solution for "None"
                 match type, but a lot of sprites are needed for others.
      * "Edges": The d-tile is cut into parts, each one representing an edge
                 of the tile. Two versions are needed for each edge
                 (matching/not matching) when match = "Edges".
      * "Corners": The d-tile is cut into parts, each one representing a
                   corner. Four versions of each corner are needed for hex
                   tilesets, height for others.
      * "Directions": One sprite is drawn for each direction. 64 sprites are
                      needed for hex tilesets, 256 for others.
                      This is mostly useful for roads-style elements.
For reference, here is a full [layer_<name>] declaration (including fields
not described above:

[layer_terrain]
type   = "Normal" ; One of "Normal", 
match  = "None"   ; One of "None", "Edges", "Corners", "Full"
sprite = "Whole"  ; One of "Whole", "Edges", "Corners", "Directions"
tiles  = "lake", "coast", "floor", "arctic", "desert", [...]
mask   = "t.dither_tile" ; This mask is applied to blend tiles together with
                         ; their neighbors when match = "None". Each layer
                         ; can use a different mask.

  Equivalence
  -----------

It is important that this design can at least emulate all features currently
supported. This is true for the following layers:
  - Terrain 1-2-3
  - Darkness & Fog
  - Water
  - Roads
  - Specials 1-2-3 (not including base flags on layer 3, they are handled
    differently)
  - Grid 1-2
  - City 1
  - Fog
  - Units
  - Focus units (except for animation)
Other layers are handled just like now (except their order is customizable).
The background is always drawn below all other layers.

  Animations
  ----------

Adding animations is something the design should support without fundamental
changes. As the animation support is currently poor, I don't think removing
it would be such a big regression. Still, here is how to restore it.
 Obviously, the map scrolling animation is not managed by the ruleset.

 # Basics

Adding animations to the above ruleset syntax is very simple. New fields are
allowed in [layer_xxx] sections, whose names are self-explanatory enough [d]:

animated:           TRUE ; Defaults to FALSE
animation_duration: 250  ; Milliseconds

An animation is rendered as a sequence of sprites. The sprites to use are
defined in individual [tile_xxx] sections, as the value of the "sprites"
field. They will be drawn in the order they appear in the list. There is no
guarantee that they will all be drawn: if the system is lagging, it will drop
frames.

The basic support as described until now allows the active units animation to
be restored.

 # Transitions

Transitions resemble events a lot. They are started whenever an object's
property changes. The list of supported properties is yet to be defined, but 
could include unit position, orientation, activity, combat, death and
load/unload; tile terrain, present extras, build progress; city size,
happiness.
 When a transition is started, the the corresponding object enters an
intermediate state (from the drawing system point of view). This state can be
used as a requirement ("Transition") for matches (code-wise, there will
likely be tables of d-tiles having such reqs). While the transition is
running, no requirement involving the given property will be fulfilled.
 Two other requirements may be used while a transition is running:
"TransitionFrom" and "TransitionTo". They do not have sense when no
transition is running (never matched). When the changing property is an
unit's position, "TransitionFrom" may be "North", "NorthWest" etc. A
transition ends when all the animations it started are over.

Supporting transitions would allow the combat and nuke animations to be
restored. It would allow transitions when units are moving, but at the
expense of a very big sprite count.

 # Moving units

Instead of listening to "Transition", a d-tile may listen to "UnitMoving" (by
using it as a requirement). "UnitMoving" requirement take the unit type name
as its argument. This will enable the effects two more rules:

[tile_xxx]
matches_before: "lake"    ; Match list
animation_move: "Offsets" ; One of "Slide", "Offsets"
animation_offsets:
  { "x", "y"
    0,   0
    4,   2
    ; [...]
  }

The optional "matches_before" list allows to check for features of the tile
the unit comes from. This allows different animations when unloading from a
transport (checking for sea -> land) or taking off.
 The two other rules describe how the sprite will be moved form one tile to
another:
  - "Slide" means that the sprites will be animated along a straight line
    going from the start tile to the destination. If only one sprite is set,
    it will move smoothly. If more are provided, only one frame will be drawn
    for each.
  - "Offsets" gives fine-grained control on where the sprites are drawn. All
    offsets are measured relative to the destination tile.

 # Probability

[layer_xxx]
animation_probability: 100  ; This is the default, values in (0, 100]

The goal of the animation probability is that each matching tile gets at any
time the given probability of actually being animated.
 More precisely, the average over time of the proportion of matching tiles
begin animated should converge to the probability. The challenge is to find
an algorithm to do that. This field is not strictly needed to support
animations, but would be useful to allow, for example, moving game.

  Notes
  -----

[a] Necessary changes are:
    - Add "display" requirements, allowing some rules to change if the req is
      used for display purposes. For example, "Extra" req type would use the
      "hiders" field of struct extra.
    - Add more requirement types.
    - (Optional) Add a ranges for directions: "North", "NorthWest", ...
[b] This is because the current format uses "match" in a similar way.
[c] Most likely, the implementation would stop trying d-tiles once it has
    found one, so only one of the d-tiles would be drawn. Using separate
    layers is the solution to go, as it clearly defines the drawing order.
[d] The animation duration is scaled by an user-defined value stored in the
    client configuration. This allows different animations speeds.

  Links
  -----

[1] http://freeciv.wikia.com/wiki/Coding


_______________________________________________
Freeciv-dev mailing list
[email protected]
https://mail.gna.org/listinfo/freeciv-dev

Reply via email to