Mocking the Interface

Overview

The terrain successfully enumerates, but it cannot be used yet. In order to load it, all functions of the terrain interface must be implemented.

This chapter teaches how to implement them with empty placeholders (mock-ups) that can later be filled with meaningful data.

Terrain

Most likely, some kind of superstructure is required to store all data related to terrain.

UAW_Extension_Terrain is a data type fully under control of the extension; UAW will never read or modify it. This example has no relevant data to place there – but it’s where you’d store global terrain data like texture atlases, file handles, etc.

struct UAW_Extension_Terrain {
	// empty placeholder
};

void UAW_CALL unloadTerrain(
	UAW_Extension_Terrain * terrain
) try {
	delete terrain;
} catch(...) {
}

UAW does not allow C++ exceptions, hence function bodies are wrapped in try-catch blocks.

Terrain initialization does not only take place in the loadTerrain() function defined earlier, but also in a second callback known to UAW as populateScenario(). This is where buildings and vehicles are added (as long as they are independent of the specific scenario, i.e. civilian ambience).

RationaleThere is a circular dependency between scenario and terrain: Any vehicle added to a scenario requires the shape of the terrain below it (for things like placing wheels correctly). So the terrain must exist before the scenario. Then, however, terrain would be unable to add any vehicles to the scenario, because the scenario doesn’t exist yet when the terrain initializes! Therefore, terrain initialization is broken up: First the bare terrain initialization, and later on anything depending on the scenario.
int UAW_CALL populateScenarioWithTerrain(
	UAW_Extension_Terrain const * terrain,
	UAW_API_ScenarioPopulate *    uaw
) {
	return 1; // nothing to do yet
}

Register the callbacks:

UAW_Extension_Terrain * UAW_CALL loadTerrain(
	void *                      extensionData,
	UAW_API_TerrainLoad *       uaw,
	UAW_Extension_TerrainID     id,
	UAW_Extension_Terrain_API * result
) {
	return nullptr; // for now, simulate failure
}
) try {
	result->unload           = &unloadTerrain;
	result->populateScenario = &populateScenarioWithTerrain;
	return new UAW_Extension_Terrain{ };
} catch() {
	return nullptr;
}

Tiles

As explained in Tile Management, UAW_Extension_TerrainTile is a data type fully under control of the extension.

In this example, the type is defined to hold the North/East coordinate of a tile. Extensions may add data like 3D models and environment state.

struct UAW_Extension_TerrainTile {
	unsigned int indexN;
	unsigned int indexE;
};

// Load extension-specific data for the given tile.
//  • return “NULL” on failure or if there is no tile at this point
UAW_Extension_TerrainTile * UAW_CALL loadTerrainTileAt(
	UAW_Extension_Terrain *     terrain,
	unsigned int                indexNorth,
	unsigned int                indexEast,
	UAW_TerrainTileProperties * properties
) try {
	return new UAW_Extension_TerrainTile{ indexNorth, indexEast };
} catch(...) {
	return nullptr;
}

// Release extension-specific data for the tile.
void UAW_CALL releaseTerrainTile(
	UAW_Extension_Terrain *     terrain,
	UAW_Extension_TerrainTile * tile
) try {
	delete tile;
} catch(...) {
}

These callbacks receive the UAW_Extension_Terrain structure that has been defined above, in order to have access to global level data.

Register these callbacks when initializing the terrain:

UAW_Extension_Terrain * UAW_CALL loadTerrain(
	void *                      extensionData,
	UAW_API_TerrainLoad *       uaw,
	UAW_Extension_TerrainID     id,
	UAW_Extension_Terrain_API * result
) try {
	result->unload           = &unloadTerrain;
	result->populateScenario = &populateScenarioWithTerrain;
	result->loadTileAt       = &loadTerrainTileAt;
	result->releaseTile      = &releaseTerrainTile;
	return new UAW_Extension_Terrain{ };
} catch() {
	return nullptr;
}

Terrain Views

Almost identical to terrain tiles: Define the type UAW_Extension_TerrainView, implement the two functions for creating and destroying terrain views. Add empty dummies for processing changes to views and for rendering them.

struct UAW_Extension_TerrainView {
	// empty placeholder
};

UAW_Extension_TerrainView * UAW_CALL onTerrainViewCreate(
	UAW_Extension_Terrain * terrain,
	int                     tileIndexNorth,
	int                     tileIndexEast,
	int                     rangeInTiles
) try {
	return new UAW_Extension_TerrainView;
} catch(...) {
	return nullptr;
}

void UAW_CALL onTerrainViewChange(
	UAW_Extension_Terrain *     terrain,
	UAW_Extension_TerrainView * view,
	int                         tileIndexNorth,
	int                         tileIndexEast,
	int                         rangeInTiles
) {
	// nothing to do yet
}

void UAW_CALL onTerrainViewDestroy(
	UAW_Extension_Terrain *     terrain,
	UAW_Extension_TerrainView * view
) try {
	delete view;
} catch(...) {
	return nullptr;
}

void UAW_CALL renderTerrain(
	UAW_Extension_Terrain const *    ,
	UAW_Extension_TerrainView const *,
	UAW_Matrix_3x3_float             worldToEye_raw,
	UAW_NED_float                    eyeInTile,
	float                            zoomScale,
	unsigned int                     widthInAnyUnit,
	unsigned int                     heightInAnyUnit,
	UAW_Clock                        utc
) {
	// nothing to do yet
}	

Registering:

	result->unload           = &unloadTerrain;
	result->populateScenario = &populateScenarioWithTerrain;
	result->loadTileAt       = &loadTerrainTileAt;
	result->releaseTile      = &releaseTerrainTile;
	result->onViewCreate     = &onTerrainViewCreate;
	result->onViewChange     = &onTerrainViewChange;
	result->onViewDestroy    = &onTerrainViewDestroy;
	result->render           = &renderTerrain;
	return new UAW_Extension_Terrain{ };

The Rest

Two functions are still missing for terrain to load successfully: Airspace and borders information. These are currently placeholders and subject to change.

// Returns the party that owns the territory at the given point, or “nullptr” if it is neutral.
UAW_Scn_Party * UAW_CALL terrainTerritoryAt(
	UAW_Extension_Terrain const *    ,
	UAW_Extension_TerrainTile const *,
	UAW_NE_float                     relativeToTileCenter
) {
	return nullptr; // neutral
}

Registering:

	result->unload           = &unloadTerrain;
	result->populateScenario = &populateScenarioWithTerrain;
	result->loadTileAt       = &loadTerrainTileAt;
	result->releaseTile      = &releaseTerrainTile;
	result->onViewCreate     = &onTerrainViewCreate;
	result->onViewChange     = &onTerrainViewChange;
	result->onViewDestroy    = &onTerrainViewDestroy;
	result->render           = &renderTerrain;
	result->territoryOf      = &terrainTerritoryAt;
	return new UAW_Extension_Terrain{ };

In order to not only make the terrain load successful, but also the scenario, a scenario must be provided just like terrain. It requires two callbacks: One for cleaning up, and one to select sky colors and renderer settings.

struct UAW_Extension_Scenario {
	// empty placeholder
};

void UAW_CALL destroyScenario(
	UAW_Extension_Scenario * scenario
) try {
	delete scenario;
} catch(...) {
}

void UAW_CALL prepareScenarioRendering(
	UAW_Extension_Scenario * scenario,
	UAW_Clock                utc
) {
	// nothing to do yet
}

UAW_Extension_Scenario * UAW_CALL createScenario(
	UAW_Extension *              extension,
	UAW_API_ScenarioPopulate *   uaw,
	UAW_Extension_ScenarioID     id,
	UAW_Extension_Scenario_API * methods
) {
	return nullptr; // for now, simulate failure
) try {
	// Pass our callbacks:
	methods->destroy          = &destroyScenario;
	methods->prepareRendering = &prepareScenarioRendering;
	return new UAW_Extension_Scenario;
} catch(...) {
	return nullptr;
}

With these callbacks, and without any other extensions, the empty scenario loads:

The weird sky colors are due to prepareScenarioRendering() not providing sky colors. In this case, UAW uses absurd presets to point towards the error.

Physics

In order to actually fly through the scenario (even though there is nothing to see), a few physics callbacks must be defined.

rayVsTerrainTile() computes the intersection of a terrain tile with a line segment (start + vector to end). UAW uses it to compute physics of tires, bullets, and planes colliding with the ground. Access is automatically forwarded to vehicles for things like radar altitude measurements.

This mock-up will always return no collision by setting the relative distance of the hit to >1.

// Computes the collision of a line segment with a terrain tile.
//  • origin and direction are meters relative to tile center
UAW_TerrainRayHit UAW_CALL rayVsTerrainTile(
	UAW_Extension_Terrain const *    ,
	UAW_Extension_TerrainTile const *,
	UAW_NED_float                    origin,
	UAW_NED_float                    direction
) {
	return { 2.f, { }, }; // relative distance >1 means “no collision”
}

terrainElevationAt() computes elevation and ground normal at a specified point. It is used to place vehicles, buildings, and explosion craters at the correct vertical position.

This version will simply return 0 m elevation and a normal pointing upwards.

// For a specific point in the terrain, computes the elevation above mean sea level (MSL), in meters, as well as the normal.
//  • elevation can be negative for some places on Earth
UAW_Lvl_ElevationAndNormal UAW_CALL terrainElevationAt(
	UAW_Extension_Terrain const *     terrain,
	UAW_Extension_TerrainTile const * tile,
	UAW_NE_float                      position
) {
	return { 0.f, { 0.f, 0.f, -1.f } };
}

Registering these callbacks:

	result->releaseTile      = &releaseTerrainTile;
	result->onViewCreate     = &onTerrainViewCreate;
	result->onViewChange     = &onTerrainViewChange;
	result->onViewDestroy    = &onTerrainViewDestroy;
	result->render           = &renderTerrain;
	result->territoryOf      = &terrainTerritoryAt;
	result->rayVsTerrainTile = &rayVsTerrainTile;
	result->elevationAt      = &terrainElevationAt;
	return new UAW_Extension_Terrain{ };

Map

Planes may try to display a terrain map. For this reason, map views must be supplied. Again, UAW_Extension_TerrainMapView is the data type fully under the control of the extension.

This example leaves it empty, but a real extension would use it to store an overview bitmap or something similar:

struct UAW_Extension_TerrainMapView {
	// empty placeholder
};

// Reserves all data for rendering an overview map of the terrain to a virtual screen.
//  • returns “NULL” on failure
//  • “destroyMapView()” will be called after use
UAW_Extension_TerrainMapView * UAW_CALL createTerrainMapView(
	UAW_Extension_Terrain * terrain,
	UAW_API_StartScreens *  uaw
) try {
	return new UAW_Extension_TerrainMapView;
} catch(...) {
	return nullptr;
}

// Draws an overview map of the terrain to a virtual screen.
//  • “range” is the radius of the mapped area, in meters
//  • “transformation” is never “NULL” and transforms the map from meters to pixels on the screen
void UAW_CALL drawTerrainMap(
	UAW_Extension_Terrain const *        terrain,
	UAW_API_Screen *                     uaw,
	UAW_Extension_TerrainMapView const * view,
	UAW_NED_double const *               center,
	float                                range,
	UAW_Matrix_3x2_float const *         transformation
) {
	// nothing to do yet
}

// Releases all data for rendering this overview map of the terrain.
void UAW_CALL destroyTerrainMapView(
	UAW_Extension_Terrain *        terrain,
	UAW_API_StopScreens *          uaw,
	UAW_Extension_TerrainMapView * view
) try {
	delete view;
} catch(...) {
}

Registering these callbacks:

	result->onViewDestroy    = &onTerrainViewDestroy;
	result->render           = &renderTerrain;
	result->territoryOf      = &terrainTerritoryAt;
	result->rayVsTerrainTile = &rayVsTerrainTile;
	result->elevationAt      = &terrainElevationAt;
	result->createMapView    = &createTerrainMapView;
	result->drawMap          = &drawTerrainMap;
	result->destroyMapView   = &destroyTerrainMapView;
	return new UAW_Extension_Terrain;

Flying

With all callbacks provided, you can fly through this scenario (given that you also have an extension installed that provides a plane):

This will, however, crash very soon due to the player leaving the scenario. The scenario enumeration from the previous chapter does not explicitly initialize the start point, so the player starts at the coordinate origin and moves exactly North. Any small turbulence throws him off West of the scenario (as indicated by the text in the screenshot above).

This can be fixed by setting the start point a few kilometers into the terrain, and by changing the heading to North East:

	// Declare an empty scenario using the sample terrain:
	UAW_ScenarioSpecs scenario = { };
	scenario.id        = sampleScenario;
	scenario.terrainID = sampleTerrain;
	scenario.name      = sampleScenarioName;
	scenario.summary   = sampleScenarioDescription;
	scenario.create    = &createScenario; // pass our callback
	// Do not spawn at the edge of the scenario but a few kilometers into it:
	scenario.spawnPoint.north = 12'000.;
	scenario.spawnPoint.east  = 12'000.;
	scenario.spawnPoint.down  = -1'000.;
	scenario.spawnHeading     = 45; // degrees
	uaw->newScenario(scenario);

Actual terrain rendering will be handled in the next chapter.

Download an updated sample project here.