StrategyVisuals constructor

StrategyVisuals({
  1. required StrategySimulation simulation,
  2. required GraphicsDevice device,
  3. int capacity = 4096,
  4. int? viewer,
  5. RenderMaterial? ground,
  6. RenderMaterial? units,
  7. RenderMaterial? buildings,
  8. RenderMaterial? unseen,
  9. RenderMaterial? remembered,
  10. UnitSize unitSize = const UnitSize(),
  11. DeviceMesh? unitMesh,
  12. DeviceMesh? buildingMesh,
  13. Vector3? buildingMeshSize,
  14. List<UnitLook> looks = const <UnitLook>[],
  15. List<RenderMaterial>? buildingSides,
  16. bool buildingStandsTall = false,
  17. MeshLook? resource,
  18. List<PropBatch> props = const <PropBatch>[],
  19. double? waterLevel,
  20. RenderMaterial? water,
  21. double groundMetersPerTexture = 8.0,
})

Builds the ground and the batch the crowd is drawn in.

capacity is the largest crowd this will draw. A batch's size is fixed when it is made — the buffer behind it is allocated once — so a game that intends thousands says so here rather than discovering the ceiling when it reaches it.

unitMesh replaces the boxy default with a real model, already scaled and grounded — its own local origin is where a unit's feet stand, not its centre, since sync() places it with no added lift once one is set. buildingMesh and buildingMeshSize do the same for a hall, but as a shared mesh rather than a shape rebuilt at each footprint: a building's width and depth come from the level document, and one upload stretched per instance with SceneNode.setScale costs nothing extra past the second building, where a fresh CuboidShape would cost one upload apiece.

looks dress units by kind, one batch per kind and side; see UnitLook. buildingSides gives each side's buildings their own material — a castle in its owner's colours — in place of buildings; and buildingStandsTall lets a shared building mesh keep its proportions, rising by as much as its footprint stretches rather than being pressed to the fixed height a box is given.

resource is drawn at every deposit, props are the map's furniture, and waterLevel floods everything below it with a sheet of water. groundMetersPerTexture is how far the ground goes before its texture repeats — the width of the map, for a texture painted to fit it.

Implementation

StrategyVisuals({
  required this.simulation,
  required GraphicsDevice device,
  int capacity = 4096,
  this.viewer,
  RenderMaterial? ground,
  RenderMaterial? units,
  RenderMaterial? buildings,
  RenderMaterial? unseen,
  RenderMaterial? remembered,
  this.unitSize = const UnitSize(),
  DeviceMesh? unitMesh,
  DeviceMesh? buildingMesh,
  Vector3? buildingMeshSize,
  List<UnitLook> looks = const <UnitLook>[],
  List<RenderMaterial>? buildingSides,
  bool buildingStandsTall = false,
  MeshLook? resource,
  List<PropBatch> props = const <PropBatch>[],
  double? waterLevel,
  RenderMaterial? water,
  double groundMetersPerTexture = 8.0,
}) : assert(
       (buildingMesh == null) == (buildingMeshSize == null),
       'a shared building mesh needs its own natural size to scale from, '
       'and a size with nothing to scale is dead weight',
     ),
     assert(
       buildingSides == null || buildingSides.isNotEmpty,
       'a building in nobody\'s colours',
     ),
     _buildingMaterial = buildings ?? _stone(),
     _buildingSides = buildingSides,
     _buildingMesh = buildingMesh,
     _buildingMeshSize = buildingMeshSize,
     // Not `this._…`: a private named parameter puts the private name in
     // the API and needs the newest language version to call.
     // ignore: prefer_initializing_formals
     _buildingStandsTall = buildingStandsTall,
     // ignore: prefer_initializing_formals
     _resource = resource,
     waterLevel = waterLevel,
     // A real model stands on its own feet at its local origin, once
     // grounded the way the strategy demo grounds its workers; the
     // default `CuboidShape` is centred instead, so only it needs lifting
     // by half its height to stand on the ground `at.y` names.
     _unitGroundLift = unitMesh == null ? unitSize.height / 2.0 : 0.0,
     _device = device {
  _ground = MeshNode(
    DeviceMesh.upload(
      device,
      meshDataOf(
        const HeightfieldGeometry().build(
          simulation.ground,
          material: 'ground',
          metersPerTexture: groundMetersPerTexture,
        ),
      ),
    ),
    ground ?? _turf(),
    name: 'ground',
  );

  _crowd = InstancedMeshNode(
    unitMesh ??
        DeviceMesh.upload(
          device,
          CuboidShape(
            size: Vector3(unitSize.width, unitSize.height, unitSize.width),
          ).build(),
        ),
    units ?? _cloth(),
    capacity: capacity,
    name: 'crowd',
  );
  _adopt(_crowd);
  for (final UnitLook look in looks) {
    _byKind[look.kind] = <int>[
      for (final MeshLook side in look.sides)
        _adopt(
          InstancedMeshNode(
            side.mesh,
            side.material,
            capacity: capacity,
            name: look.kind,
          ),
        ),
    ];
  }
  _filled = Int32List(_batches.length);

  for (final PropBatch batch in props) {
    _props.add(
      _reserve(
        InstancedMeshNode(
          batch.look.mesh,
          batch.look.material,
          capacity: math.max(1, batch.placements.length),
          name: batch.name,
        ),
      ),
    );
    _placements.add(batch.placements);
    _placed.add(List<bool>.filled(batch.placements.length, false));
    _slots.add(
      Int32List(batch.placements.length)
        ..fillRange(0, batch.placements.length, -1),
    );
  }

  final Heightfield field = simulation.ground;
  if (waterLevel != null) {
    if (_wetBounds(field, waterLevel) case (
      final double x0,
      final double z0,
      final double x1,
      final double z1,
    )) {
      _water = MeshNode(
        DeviceMesh.upload(
          device,
          PlaneShape(width: x1 - x0, depth: z1 - z0).build(),
        ),
        water ?? _pond(),
        name: 'water',
      )..setPosition((x0 + x1) / 2.0, waterLevel, (z0 + z1) / 2.0);
    }
  }

  if (viewer == null) return;
  final FogOfWar fog = simulation.fog;
  _tileTop = Float32List(fog.cellCount);
  for (var cell = 0; cell < fog.cellCount; cell++) {
    // Over a pond the fog has to cover the water rather than the bed under
    // it, or the sheet shows through a square of dark as a square of blue.
    final double top = _topOf(fog, cell);
    _tileTop[cell] = waterLevel != null && waterLevel > top
        ? waterLevel
        : top;
  }
  final DeviceMesh tile = DeviceMesh.upload(
    device,
    CuboidShape(
      size: Vector3(fog.cellSize, _tileDepth, fog.cellSize),
    ).build(),
  );
  _unseen = _tileBatch(tile, unseen ?? _night(), 'unseen', fog.cellCount);
  _remembered = _tileBatch(
    tile,
    remembered ?? _dusk(),
    'remembered',
    fog.cellCount,
  );
}