[dev] Architecture Thread

WildWeazel

Carthago Creanda Est
Joined
Jul 14, 2003
Messages
7,363
Location
/mnt/games/Civ3/Conquests/Scenarios
This is a thought experiment and case study to illustrate how we can structure the game logic using Components and Events.

First, to define terms. An Event is a function call indicating anything that happened that some other code may want to know about. These are built into the C# language, and to Godot as Signals. A Component is a class that implements a game mechanic or some utility functionality (like autosaves) that persists for the whole game session. They are ordinary C# classes stored in the ComponentManager so that they can be accessed from anywhere by type.

Let's consider one of the more complex sequences in the game: a city under siege by a stack of attacking units. The defender is the player as France, with a brand new veteran Musketeer UU and a Longbowman defending a walled town against some Trebuchets (which we'll assume do collateral damage) and Knights. The French fare very well.

The trebuchets bombard the city one by one:
  • Each raises a UnitBombard event and calls into the CombatComponent (which in turn queries CityComponent, TerrainComponent, and UnitComponent for respective combat modifiers) to resolve the outcome.
  • The Musketeer loses a total of 2 HP, raising a UnitHPLoss event each time
  • The UI layer receives the event and displays the message banners
  • On one bombardment, the CombatComponent calculates that collateral damage destroys the city walls, and directly updates the CityComponent which raises a BuildingDestroyed event
  • The UI layer receives the event and displays the message banner, and updates the city graphic

Now the first Knight attacks, triggering a series of events as the CombatComponent resolves the battle (again calling into other components as necessary):
  • CombatComponent raises UnitBattleBegin
  • CombatComponent resolves the Longbowman's defensive bombardment, Knight loses 1HP and raises UnitHPLoss event
  • CombatComponent raises UnitBattleRoundBegin
  • CombatComponent resolves the round, Knight loses 1 HP and raises UnitHPLoss event
  • CombatComponent raises UnitBattleRoundEnd
  • Repeat until Knight is at 1HP
  • During the last round resolution, Knight is able to retreat and raises UnitRetreat event
  • A final UnitBattleRoundEnd, UnitDefeat (for the Knight), UnitVictory (for the Musketeer), and UnitBattleEnd events are raised
  • During battle resolution, the Musketeer is promoted to Elite and raises UnitPromotion and UnitHPGain events
  • The UI layer receives the UnitPromotion event and displays the message banner
  • GoldenAgeComponent receives the UnitVictory event, sees that the unit has the "Triggers Golden Age" ability and the civ has not yet had a golden age, updates the civ's game state to indicate a golden age is in progress, and raises a CivGoldenAgeBegin event
  • The UI layer recieves the CivGoldenAgeBegin event, sees that it is for the player's civ, and displays the domestic advisor popup
A similar process repeats for the second Knight. This one is not so lucky.
  • During the last round resolution, the Knight is killed, raising a UnitDeath event
  • As before, a final UnitBattleRoundEnd, UnitDefeat (for the Knight), UnitVictory (for the Musketeer), and UnitBattleEnd events are raised
  • GreatLeaderComponent receives the UnitVictory event and sees that the unit is Elite, calculates that a Great Leader is made, tells UnitComponent to create a new Military Great Leader on the Musketeer's tile, and raises a UnitGreatVictory event
  • UnitComponent creates the new MGL and raises a UnitCreated event
  • The UI layer receives the UnitGreatVictory event and displays the message banner and military advisor popup

Some of these events may be superfluous, or only subscribed to by other objects outside the scope of this analysis. For example I ignored animations, which could be played in response to events or just based on state polling in the frame updates. In general I feel it's better to have too many events than too few. It doesn't hurt to raise an event that nobody is subscribed to. The choice whether to have components directly update each other or communicate between events is a bit fuzzy. I used events whenever crossing the UI-engine boundary, and when another component is not necessarily involved, but may react; direct function calls when the receiver inherently needs to update, or when return values are required.
 
Last edited:
Some potential Components, and elements that could be Components or internal classes

  • Turn (turn counter, date calculation, active player)
  • Autosave
  • History (replay log, F8-F11 screen stats)
    • Histograph
    • Palace
  • Rules (global config, scenario settings)
    • Plague
    • GoldenAge
    • GreatLeader
    • Difficulty
  • Victory
    • Spaceship
  • Unit
  • City
    • Building
    • Citizen
  • Map (tiles, terrain)
    • GlobalWarming
    • Border
    • Improvement
  • Combat (calculations and setting results)
    • Promotion
  • Diplomacy
    • Espionage
  • Faction (civ settings, all assets)
    • Barbarian
  • Science
    • Era
  • Economy (domestic advisor controls, money calculations)
    • Budget
  • Trade (resource instances, routes, availability)
  • ModScript
  • Pedia
 
On events: Although at first I thought, "that's a lot of events", my current thinking is it probably does make sense to have that many. If for no other reason than because in a scenario like you describe, the UI will want to be subscribed, and react to, many of them.

I appreciate Component being defined as well. That term is used in enough ways in computer science that my understanding of what it meant in C7 was previously fuzzy and not super accurate. Tentatively, I think it makes sense to have them to implement mechanics, and be able to listen to events.

The other thing that occurs to me is that there's going to need to be more engine -> front end action than I'd thought about previously.

Which brings me to a question - what are your thoughts on engine/front-end (Godot) separation? I've been pushing in the direction of keeping them separate where possible, both because the AI should be able to implement all mechanics separate from the front end (and that separation helps ensure that), and to help avoid tying ourselves to heavily to Godot 3. I'd created the C7Engine/EntryPoints classes as kind of an API between the front end and the engine (albeit an early-subject-to-change API). But with the realization that we may have events flying all over in both directions, I'm wondering how feasible it will be to keep the engine separate from the UI layer. It still seems like it would be a good idea to keep their responsibilities separate, and it could even give useful new features, such as Civ4's auto-simulate-turns feature. But I'd be curious to hear your thoughts on it. Should all the components live in the engine, so they can function independently from the UI?
 
Philosophically I want as much separation from the game logic and the Godot side as possible, but I'm starting to really question how feasible that is. I don't really have any answers or directions to suggest; I'm just realizing a complex yet user-friendly game UI is a beast I haven't faced before. We're way past textboxes, radio buttons, and RESTful APIs! Or even left/right, thrust, fire, and hyperspace buttons.

Just showing which resources are tradable requires checking the player's tech tree, trade routes (including possible blocked routes), and a few other game state items (war? embargoes?), and my brain is starting to melt trying to imagine how that all comes together in a UI. I can imagine class interfaces for all that info on a value-by-value basis, but the plumbing frightens me. (Or maybe I'm overthinking it...I did manage a spoiler-free tech trade availability list in CIA3.)
 
Here are some 200 different events I've brainstormed, probably still forgetting some mechanics. The naming conventions and granularity are a bit inconsistent but you get the idea.

These are event instances, not necessarily unique data types. That is, these are cases where an event would be raised. Some of them could share types, or potentially be broken into different instances/types for different cases. Basically any time something happens/changes in the game state data that some other code may want to know about, there should be an event. The actual type of the event raised, and its parameters, are TBD and will probably be decided as we get to each component. There's some tradeoff between very specific events, which make for more instances and listeners and types to keep track of, and more general events, which require more parameters and filtering to determine if it's relevant to the listener.

I'm not including UI events (button presses, etc) since those are self descriptive and they generally won't reach the game layer, but will be handled in Godot and translated to a regular function call.

Code:
GameStateEvent
    GameStart
    PlayerTurnStart
    PlayerTurnEnd
    GameTurnAdvance
    WorldLeaderElection
    WorldLeaderElected
    TurnLimitReached
    PlayerVictory
    PlayerDefeat
    GameEnd
CivEvent
    DiplomacyEvent
        DiplomaticFirstContact
        DiplomaticOpinionChanged
        DiplomaticStateChange
        DiplomaticDealMade
        DiplomaticDealExpired
        DiplomaticDealBroken
        DiplomaticEmbassyEstablished
        EspionageMissionAttempted
        EspionageMissionSucceeded
        EspionageMissionFailed
        ResourceTradeBegin
        ResourceTradeEnd
        WartimeBegin
        WartimeEnd
    BudgetAllocated
    TreasuryChange
    TreasuryDepleted
    ResearchChanged
    ResearchQueued
    ResearchProgress
    ResearchCompleted
    TechAcquired
    EraEntered
    GoldenAgeStarted
    GoldenAgeEnded
    ResourceAcquired
    ResourceLost
    GovernmentChanged
    MobilizationStarted
    MobilizationEnded
    CapitalChanged
    PalaceUpgraded
    VictoryPointChange
    TradeConnected
    TradeDisconnected
WorldEvent
    TileEvent
        TileVisibilityChange
        TileDiscovered
        TileTerrainChange
        TileImproved
        TileImprovementLost
        TilePillaged
        TileBombarded
        TileCratered
        TilePolluted
        TileErupted
        TileResourceDiscovered
        TileNuked
        TileResourceDepleted
        TileResourceConnected
        TileBonusPopped
        TileCampDispersed
        TileCampSpawned
        TileUprisingSpawned
    GlobalWarming
    PlagueStart
    PlagueSpread
    PlagueEnd
UnitEvent
    UnitCreated
    UnitUpgraded
    UnitPromoted
    UnitGainHP
    UnitLoseHP
    UnitDisease
    UnitOrderEvent
        UnitOrderGiven
        UnitMoveStart
        UnitMoveIntoTile
        UnitMoveInterrupted
        UnitMoveStop
        UnitBeginJob
        UnitCompleteJob
        UnitJobInterrupted
        UnitRecon
        UnitRebase
        UnitDoIntercept
        UnitParadrop
        UnitDrop
        UnitLoad
        UnitUnload
        UnitPillage
        UnitStateChange // active, waiting, fortified, sentry, loaded, moving, fighting, ended, dead
        UnitFortify
        UnitSentry
        UnitWait
        UnitSkip
        UnitWake
        UnitFidget
        UnitDisband
        UnitSacrifice
        UnitEscort
        UnitBuildCity
        UnitJoinCity
        UnitActivated
    CombatEvent
        BattleEvent
            BattleStart
            BattleRoundStart
            UnitAttackStart
            UnitDefendStart
            UnitWinRound
            UnitLoseRound
            BattleRoundEnd
            UnitAttackEnd
            UnitDefendEnd
            UnitRetreat
            UnitDeath
            UnitVictory
            UnitDefeat
            UnitAdvance
        UnitGreatVictory
        UnitZOCEngage
        UnitBombard
        UnitCollateralDamage
        UnitNuke
        UnitExpend
        UnitBomb
        UnitIntercept
        UnitShootDown
        UnitStealthAttack
    UnitTurnEnd
CityEvent
    CityFounded
    CityFoodAllocated
    CityFoodStored
    CityFoodConsumed
    CityPopGrowth
    CityPopStarve
    CityPopReduced
    CitySizeIncreased
    CitySizeDecreased
    CityGrowthCapped
    CityTileWorked
    CityTileAssignmentChanged
    CitySpecialistChanged
    CitySpecialistWorked
    CityProductionChanged
    CityProductionQueued
    CityProductionAllocated
    CityProductionCompleted
    CityProductionRushed
    CityCultureAllocated
    CityCultureBorderChange
    CityCommerceAllocated
    CityPollutionGenerated
    CityDrafted
    CityWarWearinessChange
    CityResourceDemanded
    CityHappinessStateChange
    CityUnrestStart
    CityUnrestContinue
    CityUnrestRiot
    CityUnrestEnd
    CityCelebrationStart
    CityCelebrationContinue
    CityCelebrationEnd
    CityResistanceStart
    CityResistanceReduced
    CityResistanceEnd
    CityCultureFlip
    CityCaptured
    CityChangeOwner
    CityRazed
    CityAbandoned
    CityLost
    CityGovernorChange
    CityTradeConnected
    CityTradeDisconnected
    ConstructEvent
        BuildingAdded
        BuildingSold
        BuildingDestroyed
        BuildingObsoleted
        BuildingLost
        BuildingEffect
        WonderStarted
        WonderCompleted
        WonderCaptured
        WonderDestroyed
        WonderObsoleted
        SpaceshipPartCompleted
        SpaceshipPartDestroyed
        SpaceshipLaunched
 
WildWeazel said:
So "my" events will probably bubble up to Godot to make the UI respond, but not vice versa.

This is one of the really interesting questions IMO. Taking the CombatEvents, for example, or UnitBuildCity, how do we account for the fact that there could be dozens of them firing off, and the animations can't all run at the same time?

It occurs to me that it may work to simply queue them up in Godot as the engine reports them, at least until one occurs where the engine needs to wait for a player response. With combat and AI movement, for example, the engine can report 700 events to Godot, and Godot can play the animations (or not) based on map visibility and player preferences for animations. In a single-threaded case, this could take the form of the AI running everything it can until it either hits something that needs a player response, or the AI turn is over, and then the Godot UI playing animations for everything as appropriate. In a multi-threaded case, with a thread-safe queue, the UI could be playing animations for early parts of the AI turn while the AI is running calculations for later parts of the AI turn.

[thinking through things as I write intensifies from this point forward, these ideas may be half-baked]

So how would those cases where there is a player input work? I'll try to think through a couple of them.

Great Leader creation. The AI attacks your Elite Pikeman, you get the great leader Suppiluliumas (we've all been playing the Hittites lately, right? also just a great name). Some variant of UnitCreated fires and is reported to the Godot queue. When Godot reaches this event, it pops up the "Rename Victorious Unit" pop-up. But it can just report the name back to the engine, and the engine can update the unit name, at some later point. There's no need for the engine to wait for a custom name.

Diplomacy. Let's say Montezuma contacts you, the Inca, and wants you to sign an alliance against Smoke Jaguar. You are both at peace right now. If you agree, Montezuma is going to send his forces across the border. If you don't agree, maybe Montezuma decides it's not worth starting the war without some allies.

This is a really interesting case because in single player, you could pause the AI at this point, and it could wait for Godot to catch up with any previous animation events (assuming the 2+ thread model), and once the player response to the DiplomacyEvent, Montezuma could decide what to do. Maybe the player even says, "what if I throw in some Iron and we declare war against both Smoke Jaguar and Hiawatha?" - in that case Montezuma might split his forces up between two fronts, and invade both.

In multiplayer though, that doesn't work. Let's say the human Inca player decided now was a good time to make a cup of tea, and is AFK. Everyone else is waiting for a human who's AFK, including the AI. Montezuma is going to have to decide what to do without waiting for the Sapa-Inca.

The idea that comes to my head to sidestep this problem is a slight difference from how Civilization works. That is, to have military alliances, mutual protection pacts, etc. be effective from the start of the next turn. Montezuma isn't going to ask, "should we start a war?" and start holding his breath for an answer, making all his generals do nothing in the meantime. He's going to send you an envoy with proposed terms, and in the meantime have his generals make final preparations for if the Inca approve of the alliance. If he's wanting an alliance, maybe he is already making some preparations several turns ahead of time.

Heck, maybe he even says, instead of "should we start a war next turn?", "should we start a war in five turns?" There have been quite a few cases in Civ where an AI has proposed an alliance, and I've thought, "Well, I agree it would be good to punish the Maya, but all my troops are over on the American border. We're gonna need a few turns here."

The Inca could respond in the affirmative to the Aztec proposal, ask for more time, or decline it outright. If they agree to declare war in five turns, for example, there could be an option to cancel the alliance if external events change the situation. Let's say the Aztecs and Inca agree to punish the Maya in five turns, but three turns later the Spanish show up. Maybe there's a "confirm we're still going to war" event/diplomacy scenario if someone else starts a war, and an option to propose a cancellation of the alliance outright if for whatever reason things change for one side (maybe the Incan iron depletes and they get cold feet).

It might be Civ V that has this sort of a war-in-10-turns feature; I think I've seen it in another 4X, and maybe a Civ one.

-----

Thoughts? Are there other events, beside diplomacy, where a human response is needed right away during the AI turn? If there are few or no other ones, I kind of like the idea of keeping the AI going, queuing up front-end events that can be animated in real time, and basically separating the two, while sidestepping the issue. If there are a lot of other ones, that may not be feasible.

Or maybe we do have it pause the AI so it more closely matches Civ; maybe it's an option. Maybe it doesn't pause in multiplayer, so Montezuma is effectively always asking for an alliance that will start on the human player's turn, if there's a human involved. That somewhat sounds like how Civ4 MP works IIRC? It's been so long since I played it though.
 
This is one of the really interesting questions IMO. Taking the CombatEvents, for example, or UnitBuildCity, how do we account for the fact that there could be dozens of them firing off, and the animations can't all run at the same time?
To start we'll run one animation at a time but eventually we'll need to be able to run many. Imagine an extreme example: a multiplayer game with players on two continents, and one of them orders a large stack attack. It would be intolerable for the players on the other continent to sit there for multiple minutes unable move any units while the animations play out. We'll need to allow for non-conflicting animations to play simultaneously.

These animation rules also have implications for gameplay. For example if a player stack-attacks a city filling the animation queue with combat anims, and we only allow one animation to play at a time, the defender won't be able to reinforce the city because any movement command would be blocked waiting for a move anim to play after the attacks are finished. It's been so long since I've played Civ 3 MP that I don't remember well what the original game would do in this case. I think it lets you add units to a tile while it's being attacked. If so we'll definitely want to allow for that.



I've been thinking about these sorts of things over the past couple of weeks, about how the engine should interact with the UI. I've changed my mind on async/await. The problem with it and other approaches like coroutines are that they bleed into the gameplay code. In other words, if you're deep in the engine, say writing some AI logic, and want to move some unit along some direction, you should be able to accomplish that with unit.move(direction), or something equivalently simple. This is important for our own sanity and it also helps make the game moddable so modders don't have to learn some fancy system for doing simple things like moving units.

Async/await couldn't provide that simple function call since it would require move to be an async function, then in order to await it the caller would also have to be async, and so would its caller, all the way up the call stack. Coroutines are limited in a similar way. In that case move would yield return something like a PlayerAction and again all callers would have to accommodate that all the way up the callstack. The best solution is to block the engine thread(s) inside move while the animation plays.

I'm going to put together a prototype threaded architecture next since further work on unit animations requires it. (Actually there are a few other things I could do with animations but they're either too complicated or uninteresting.) It'll help if we have something concrete to talk about. These purely theoretical discussions quickly reach the limit of their usefulness. My plan for the prototype is to have separate UI and engine threads communicating by passing messages back and forth. When the engine must wait for the UI, it will hang its thread on a semaphore that it passes to the UI inside a message. At the appropriate time (e.g. an animation completing) the UI will trigger the semaphore, releasing the engine thread to continue. This is relatively simple so it shouldn't take me more than a couple of days to write. One complication is that we'll need a mutex guarding the game state once we have multiple threads accessing it, and we'll need to be careful about any references to the game state held by the UI.
 
I had some self-directed training time last month, which I chose to spend on software architecture. It was helpful, and while it didn't really change my idea of the architecture much it did give me some better terms and ways of thinking about parts of it. Meanwhile I have a bunch of thoughts and open questions kicking around in my head about: terrain data; async/await in the UI, events between layers, and the possibility of a client-server architecture; system architecture diagrams and corresponding code organization; and how to start molding our code into even-driven components. As always, I tend to delay putting things in writing until I have time to fully address it all, and then just fall further behind. I'm really trying to break that habit. But it looks like it will have to wait little longer still. My grandmother-in-law just passed away, and we're flying out to the funeral this week so I'm scrambling to get ahead of everything else.
 
I'm sorry to hear about the funeral, WildWeazel.

What you wrote sounds generally agreeable, Flintlock. I agree that it helps to have examples to refer to. But I'm somewhat the opposite of WildWeazel in that I tend to create solutions as I go and only try to come up with comprehensive solutions once I've realized the shortcomings of my earlier approaches. Which isn't great for designing missions to outer space, and probably doesn't result in the best architecture in general. I'm more likely to under-architect than to over-architect, or at least to under-architect until I start feeling the pain of that under-architecting.
 
My grandmother-in-law just passed away, and we're flying out to the funeral this week
My condolences.



This is relatively simple so it shouldn't take me more than a couple of days to write.
This whole thing turned out to be trickier than I thought but it captured my interest strongly enough that I was still able to pound it out in two days. Here's how it works in more detail. I created MessageToEngine and MessageToUI types to represent messages going in either direction. The engine creates two ConcurrentQueues to store incoming and outgoing messages and spawns a thread to process incoming messages as they arrive. Sending a message to the engine from the UI looks like this (this example moves a unit with some GUID along some direction):
Code:
new MsgMoveUnit(guid, dir).send();
This is now all that UnitInteractions.moveUnit does. I'm thinking of removing moveUnit and all the other UnitInteractions functions that have become one-liners but for now I'm leaving them there so the rest of the UI code doesn't have to change as much. In general I don't like keeping around functions that do nothing but delegate work to another. Also, on another matter of style, I chose to make the messages self-sending which looks a little bit weird but it's what an OOP purist would want. We've never discussed how far we want to go down the OOP rabbit hole. Personally I don't have a high opinion of OOP purism but I like to play around with it from time to time.

Anyway, the send function adds the message to the engine's incoming queue and signals a semaphore (technically an AutoResetEvent object, a kind of semaphore) triggering the engine to check the queue. Messages flowing the other direction work the same way except there's no semaphore, the UI needs to check its message queue periodically, which right now it does in Game._Process. Since moveUnit now only sends a message, I moved the code that actually does unit movement into an extension method for MapUnit. It's in C7Engine/MapUnitExtensions.cs. The nice thing about using an extension method is that it lets us move units with unit.move(direction), like I mentioned before, from within the engine. I modified the barbarian AI to use that method meaning the code's a bit neater but more importantly the barbs now play their running animations as they move.

AnimationTracker can no longer call a callback for animations once they complete. Instead it can trigger an AutoResetEvent. The engine uses this to wait for animations. One consequence of this is that there's now no easy way for the UI to wait for animations, but I don't think it should ever need to. I moved the city building code into the engine where it belongs, the UI now sends a buildCity message to activate it. In accordance with this new setup, I've removed the engine's direct access to AnimationTracker. Now in order to start an animation it must pass a message to the UI. There's a helper function for that, the extension function MapUnit.animate:
Code:
public static void animate(this MapUnit unit, MapUnit.AnimatedAction action, bool wait)
{
    new MsgStartAnimation(unit, action, wait ? EngineStorage.uiEvent : null).send();
    if (wait) {
        EngineStorage.gameDataMutex.ReleaseMutex();
        EngineStorage.uiEvent.WaitOne();
        EngineStorage.gameDataMutex.WaitOne();
    }
}
Pretty straight forward, no? uiEvent is the AutoResetEvent that the engine thread uses when it must wait for the UI. Right now we only need one AutoResetEvent for this purpose since we only have one engine thread, but eventually we'll need more.

Note the game data mutex in the code above. The engine must release it while it waits since otherwise the UI can't access the game state. To make race conditions harder to program I added the UIGameDataAccess class. Instances of that class provide the UI with access to EngineStorage.gameData, locking the game data mutex on creation and releasing it on disposal. UIGameDataAccess implements IDisposable and is intended to be used inside using blocks. I've scattered those blocks throughout the UI, in Game._Ready, Game._Process, LooseView._Draw, and Game._UnhandledInput, though that still doesn't cover everything. The UI still calls directly into the UI to process the interturn, that needs to be done through a message instead. Also I'm uneasy about CurrentlySelectedUnit, which is a reference directly into the game data. I think it's only accessed while the game data mutex is locked but to be safe we should store it as a unit GUID instead of a MapUnit.

That's it. There are still some bugs to work out, for example right now when you move a unit it sometimes appears for a split second on the target tile before the animation starts playing. This is because the engine processes the move first then sends a message to start the animation, and the UI will sometimes draw the game state, with the unit on the target tile, before it sees the message starting the animation. But the core of the system is in place. Thoughts?
 
That sounds fairly good, and addresses some of the concerns I had about the engine/UI interaction before, with the animation tracker. Skimming through what I believe to be the most appropriate diff (https://github.com/C7-Game/Prototype/compare/UnitAnimation...EngineThreadedArch), I like a lot of what I see. The using blocks seem to offer a nice way to reduce the risk of old data hanging around in the UI and causing bugs. Having the engine as a separate thread is desirable. I like the MapUnitExtensions. I've used extension functions in Kotlin but didn't know the syntax in C#, and hadn't run across an area where I wanted it enough to figure it out.

MapUnit.animate makes sense now that I looked up that WaitOne() in C# will just wait until it acquires the mutex. Wonder why Microsoft calls it WaitOne instead of just Wait?

I don't consider myself a purist for any programming paradigm; IMO the best code (across a large project, at least) often borrows from multiple paradigms as needed. I guess you could call that a pragmatist approach. In general I tend to like object-oriented more than the average paradigm, although perhaps that says more about the zealousness of most functional advocates I've met and my aversion to zealousness. But looking at the MessageToEngine class, I like how it works here. All the message to engine types have their own process method, they're constructed with the data they'll need, and by calling send() they get queued up.

Perhaps most of all, I like the new unit move method in MapUnitExtensions, and how both player moves and barbarian moves move through there. It comes together nicely.

--------------

All in all, I'm thinking it probably makes sense to merge in these changes. The branching went Unit Animation builds on Development, EngineThreadedArch on UnitAnimation, right? If so, EngineThreadedArch is 76 commits ahead of master, which is too many, and a net of almost 1500 lines, which is also too many, but most of the difference is UnitAnimation versus Development.

Looking at things from a high level, most of UnitAnimation falls into two camps:

- Shader stuff that I am probably the least knowledgeable person about. Trying to properly review it is energy-sapping, and no one else has, so I'm thinking it's best to merge it since it seems to work great functionally, and I'll learn through experience; I learn much better through experience and modifying and studying code as I use it than through reviews (especially long reviews) anyway. Maybe it'll work as is, or maybe we'll make future PRs to change aspects of it, but it's a major Babylon feature and I don't see a good reason not to merge it, especially since a lot will want to build on it, even if indirectly.
- Architectural communication areas that made me uneasy, but are largely addressed by the EngineThreadedArch improvements. What's not, again, can be dealt with in future, smaller PRs.

As for the smaller things I commented on some in December, I think they're all outweighed by the fact that the slow review process, IMO, derailed our momentum. Long, detailed reviews drain my energy and motivation (hence why I've drifted to other projects over the past month), and they also resulted in the situation where we have 76 commits and nearly 1500 net lines waiting unmerged for over a month (for some part of it, anyway). Most of the smaller things were stylistic items anyway, and even if we do want to change them later, they're less important than being able to move forward with the big-picture items.
 
Wonder why Microsoft calls it WaitOne instead of just Wait?
I was wondering that too, though not enough to look it up.
All in all, I'm thinking it probably makes sense to merge in these changes. The branching went Unit Animation builds on Development, EngineThreadedArch on UnitAnimation, right?
Right. I considered branching EngineThreadedArch off of Development, but that doesn't make sense since the main point of doing the threaded arch right now is so that the engine can wait on the UI, and right now the only things to wait on are animations. So it would be pointless to try to implement the threaded arch without animations.

Also I agree with just merging this stuff (obviously I'm a bit biased since I wrote it). We need to get these basic architectural matters settled so work can begin on the game logic. At the very least we should agree on an interface, like unit.move(direction) to move units as I've been talking about, so we can write some game logic against that and swap out the implementation later if necessary.
 
I'm taking Puppeteer's liking of my post as implicit approval of merging, as well.

I like unit.move(direction). One call-out is that it's great for the current num pad movement, but at some point we'll need a version that accepts coordinates, for when the user tells a unit to go a long ways a way with the mouse. But I don't see any harm in having a convenience method for one tile away and another one for a longer distance away.

Hopefully we can also be more consistent in our unit pathing than Civ3 is. Just the other day I was playing Civ3, and had a stack of units selected, and selected Go To, and hovered over a tile next to an enemy city. It said it could get there in one turn via a road. Awesome. I told them to go there. They decided to walk through the un-roaded forest tile next to the tile with the road instead. :wallbash: So they arrived a turn late. It didn't wind up making a difference in the war, but it was definitely annoying that the Go To tool indicated one path, and they wound up taking a different, less efficient one.
 
I like unit.move(direction). One call-out is that it's great for the current num pad movement, but at some point we'll need a version that accepts coordinates, for when the user tells a unit to go a long ways a way with the mouse. But I don't see any harm in having a convenience method for one tile away and another one for a longer distance away.
The long distance move method will probably use the neighboring-tile move method anyway so it's a good starting point. As for naming, I like "step" for the short move method, but that might not make it clear what the method does. We could be explicit and call it something like "moveIntoNeighboringTile" but I like to keep names short for commonly used methods, like this one presumably will be.
Hopefully we can also be more consistent in our unit pathing than Civ3 is.
In my experience Civ 3 is better than most games with this sort of thing. I was playing a game of Humankind recently and several times it gave me bad info about what path a unit was going to take or how far it could move in a turn. As I recall this sort of thing also happens in Civ 6. I wonder why these sort of bugs are so common, it doesn't seem like it would be hard to program movement so it's consistent and predictable. Maybe we'll find out when we try it ourselves, or hopefully we won't have to.
 
Maybe we will! I'd think that the problem would come down to having two separate methods for the same basic thing. Which in itself isn't too surprising - the pathing preview you get assumes nothing gets in the way, but when the unit is actually moving, another civilization's unit might step into the path, necessitating it to take a different route. But it should be consistent immediately after the order is given.

Civ3 isn't bad in this regard, you're right, and Civ4 isn't either. I'm not sure when the last time before this week was that it happened in one of my games.
 
Let me just leave this here while I catch up on the rest of the comments. This is a pretty standard layered architecture but for two complications:

- Scripts (one aspect of the mod API, but also how I imagine we provide a lot of the one-off game rules for C3C mode) interact with both the UI and Core layers, by registering event callbacks and then calling public functions to change things. This is a common mod pattern from what I've seen.
- At the bottom we have a split between the two file interfaces, both of which depend on (and declare/implement classes of) the common data types. It's not very clear here but the Core shouldn't know or care which one it's using, and depend only on the data types directly. Maybe there should be a data abstraction layer in between, responsible for creating and delegating to the right file handlers.

Screenshot from 2022-01-23 23-52-54.png


This diagram was created with draw.io and the source file is in a new repo called GoodyHut.
 
As far as GUIDs, WW seemed to have an object model firmly in mind that relied heavily on object GUIDs, but I have yet to understand what that looks like and how fundamental is to various areas of code. In my early code I just designed interfaces of IEquatable<T> and used ints as a stand-in and then stuff a GUID in there later when they exist. Also, it's not clear to me how important this is to him, but when we were tossing about architectural model ideas a few months ago it seemed to be important then.
(Quote from the decision making thread, it makes more sense to discuss this here.) I'm interested to hear about this object model, and why it requires GUIDs as opposed to something simpler like serial numbers. I object to GUIDs because they have several minor problems and one major one. The minor problems are that they're too long, user-unfriendly, expensive to compare, and contain no information. The major problem is that they need to be explicitly synchronized among clients in a multiplayer game. That's by design ofc., GUIDs are supposed to be unique across all space and time. But that just means they're the wrong tool for the job, we don't want game objects to be totally unique, we need them to be shared among all players in an MP game. Serial numbers don't have this problem, since unit spawning must be synchronized anyway, all clients will agree on which is the n-th unit to be spawned.
 
Liking Flintlock's post not because I have strong opinions on GUIDs versus serial numbers at this point, but because it's a well-worded question.

The diagram in post #16 makes sense to me, but I'm not sure that there's anything I didn't already know in it (maybe that's a good thing in the "it wasn't supposed to cause any surprises" sense). The part that would help me understand things more would be having a few more examples of the scripts. I've followed Puppeteer's SimpleGame AI examples, with Lua scripts (or just C# code, depending on the preference) for AI. But never having integrated user scripts of any complexity into a project before, it's still a relative blind spot for me. Implementing rules via scripts - how that's done, how we account for all the data the script might need to evaluate to come to a decision, etc. I tend to be an example-driven learner, so the jump from diagram to code examples is beneficial.
 
(Quote from the decision making thread, it makes more sense to discuss this here.) I'm interested to hear about this object model, and why it requires GUIDs as opposed to something simpler like serial numbers. I object to GUIDs because they have several minor problems and one major one. The minor problems are that they're too long, user-unfriendly, expensive to compare, and contain no information. The major problem is that they need to be explicitly synchronized among clients in a multiplayer game. That's by design ofc., GUIDs are supposed to be unique across all space and time. But that just means they're the wrong tool for the job, we don't want game objects to be totally unique, we need them to be shared among all players in an MP game. Serial numbers don't have this problem, since unit spawning must be synchronized anyway, all clients will agree on which is the n-th unit to be spawned.
edit: quoted the wrong post

Fire away! I had mentioned GUIDs very early on but may not have explained, which may implied a stronger endorsement than I intended. I'm not married to GUIDs per se, but I do think all "content" should have a unique identifier of some kind beyond a name. That serves two purposes: one, it allows you to more elegantly reuse and/or change names and still know you're referring to the right thing elsewhere (flavor units for example, though there may be may other solutions for those); and two, it's one way of allowing for more modular mods (ie, as in Civ5-6 and many other games where mods are something you install/enable individually rather than select one to play) by reliably referencing some particular element instead of again relying on the name turning up the right thing. Having said all that, it seems something akin to a fully qualified class name would work just as well, as long as you're not changing it.
 
Last edited:
I'm not married to GUIDs per se, but I do think all "content" should have a unique identifier of some kind beyond a name.
I agree every game object needs a unique identifier. That's an interesting mention about mods referring to objects from other mods, I was only thinking about objects created over the course of a game. Those are two similar but annoyingly different problems. I'm not sure if it's even best to use the same mechanism to identify objects in both cases. For example we could use GUIDs for things defined in the scenario like techs and resources but use strings generated from serial numbers for units and cities. Although when we're loading a Civ 3 mod we won't have any GUIDs to use and can't simply generate some since then they'll be different each time the mod is loaded, defeating the point. We could use serial numbers again but tagged with the object type & name of the mod, but the mod name is not guaranteed not to change. I'm not sure about all this, I'll have to think about it some more.

I brought up the GUIDs in the first place because I wanted to fix up the unit storage code before starting on combat. Right now the game holds a list of units and must search it one at a time whenever it needs to find one with a particular GUID, which it often does. That was written months ago as a temporary thing. I want to replace that list with a hash table and figured it makes sense to replace the GUIDs at the same time.
 
Top Bottom