How to Implement Custom Unit Actions in Unciv: A Complete Developer Guide
To implement custom unit actions in Unciv, define a new UnitActionType enum entry in UnitAction.kt, create a builder function that returns a Sequence<UnitAction>, and register it in the actionTypeToFunctions map within UnitActions.kt.
Unciv is an open-source, Android-and-desktop reimplementation of Civilization V. Extending its unit action system allows modders and developers to add new buttons to the unit control panel without modifying core UI logic. The architecture follows a clear three-layer pattern separating static definitions, runtime instances, and factory assembly.
Understanding the Unit Action Architecture
Unciv’s unit action system consists of three core components that work together to render buttons and handle clicks.
UnitActionType is an enum defined in core/src/com/unciv/models/UnitAction.kt that acts as a static catalogue. It stores the default label, icon provider, keyboard binding, and default page placement for each action type.
UnitAction is a data class in the same file that represents a concrete instance for a specific unit. It carries the runtime title, enabled state, and the lambda that executes when the player presses the button.
UnitActions is an object in core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt that serves as the factory. It queries the game state, delegates to builder functions, and returns a Sequence<UnitAction> that the UI consumes.
Step-by-Step Implementation Guide
Follow these steps to add a custom action. The examples below demonstrate creating an "Inspect Unit" action that displays a popup with unit statistics.
Define the Action Type
Add a new entry to the UnitActionType enum in core/src/com/unciv/models/UnitAction.kt.
InspectUnit(
"Inspect Unit",
{ ImageGetter.getUnitActionPortrait("Info") },
isSkippingToNextUnit = false,
defaultPage = 0
)
The constructor parameters specify the translation key, an optional icon provider, whether the game should auto-skip to the next unit after activation, and the default page index for the paging UI.
Create the Action Builder
Implement a builder function that returns a Sequence<UnitAction>. Place this in core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt or a new file.
internal fun getInspectUnitActions(unit: MapUnit, tile: Tile) = sequence {
val title = "Inspect ${unit.baseUnit.name}"
yield(
UnitAction(
type = UnitActionType.InspectUnit,
useFrequency = 120f,
title = title,
action = {
ConfirmPopup(
GUI.getWorldScreen(),
"HP: ${unit.health}\nMoves: ${unit.currentMovement}/${unit.getMaxMovement()}",
"Close"
).open()
}
)
)
}
The useFrequency float determines sorting order; higher values appear earlier in the list. The action lambda receives execution context when the button is clicked.
Register the Builder
Link the builder to the enum constant in core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt inside the actionTypeToFunctions linked map.
private val actionTypeToFunctions = linkedMapOf<UnitActionType, (unit: MapUnit, tile: Tile) -> Sequence<UnitAction>>(
// ... existing mappings ...
UnitActionType.InspectUnit to UnitActionsFromUniques::getInspectUnitActions,
// ... end of map ...
)
Once registered, UnitActions.getUnitActions() automatically includes your custom button when enumerating available actions for a selected unit.
Configure Page Placement (Optional)
To dynamically control which page the button appears on based on unit state, add an entry to actionTypeToPageGetter in UnitActions.kt.
UnitActionType.InspectUnit to { unit ->
if (unit.isFortified()) 1 else 0
}
This example displays the button on page 2 when the unit is fortified, and page 1 otherwise.
Write Unit Tests
Verify the integration in tests/src/com/unciv/uniques/UnitUniquesTests.kt.
@Test
fun `Inspect Unit action is present`() {
val unit = TestUtils.createUnit("Warrior", civInfo)
val actions = UnitActions.getUnitActions(unit, UnitActionType.InspectUnit)
assertTrue(actions.any { it.type == UnitActionType.InspectUnit })
}
Run ./gradlew test to ensure the new functionality does not break existing unit action logic.
Complete Code Example
Here is the full implementation showing all three files modified:
// core/src/com/unciv/models/UnitAction.kt
enum class UnitActionType(
val label: String,
val imageGetter: () -> Image?,
val isSkippingToNextUnit: Boolean = false,
val defaultPage: Int = 0
) {
// ... existing entries ...
InspectUnit(
"Inspect Unit",
{ ImageGetter.getUnitActionPortrait("Info") },
isSkippingToNextUnit = false,
defaultPage = 0
);
}
// core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt
internal fun getInspectUnitActions(unit: MapUnit, tile: Tile) = sequence {
yield(
UnitAction(
type = UnitActionType.InspectUnit,
useFrequency = 120f,
title = "Inspect ${unit.baseUnit.name}",
action = {
ConfirmPopup(
GUI.getWorldScreen(),
"HP: ${unit.health}\nMoves: ${unit.currentMovement}/${unit.getMaxMovement()}",
"Close"
).open()
}
)
)
}
// core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt
private val actionTypeToFunctions = linkedMapOf<UnitActionType, (unit: MapUnit, tile: Tile) -> Sequence<UnitAction>>(
// ... existing mappings ...
UnitActionType.InspectUnit to UnitActionsFromUniques::getInspectUnitActions
)
With these changes, the game displays an "Inspect Unit" button in the unit actions table whenever a unit is selected.
Summary
- Define a static
UnitActionTypeentry with label, icon, and page metadata. - Build a function returning
Sequence<UnitAction>containing the runtime logic and click handler. - Register the builder in
UnitActions.actionTypeToFunctionsto enable discovery. - Customize page placement via
actionTypeToPageGetterif dynamic positioning is required. - Test using the existing test suite in
UnitUniquesTests.ktto maintain stability.
Frequently Asked Questions
What is the difference between UnitActionType and UnitAction?
UnitActionType is the enum constant representing the static definition of an action type, including its default label and icon. UnitAction is the runtime instance created for a specific unit, containing the actual title, enabled state, and the executable lambda that runs when the player clicks the button.
Where should I place custom action builder functions?
Place builder functions in core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsFromUniques.kt alongside existing implementations like getParadropActions() and getRepairActions(). Alternatively, create a new file in the same package and import it into UnitActions.kt for registration.
How do I control which page my custom action appears on?
Set the defaultPage parameter in the UnitActionType constructor for static placement. For dynamic placement based on game state, add a lambda to actionTypeToPageGetter in UnitActions.kt that returns 0 for page one, 1 for page two, and so on.
How do I test custom unit actions in Unciv?
Create unit tests in tests/src/com/unciv/uniques/UnitUniquesTests.kt. Instantiate a test unit using TestUtils.createUnit(), call UnitActions.getUnitActions(), and assert that your custom action type appears in the returned sequence. Run ./gradlew test to validate against the full test suite.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →