1. Firaxis celebrates the "Asian American and Pacific Islander Heritage Month", and offers a give-away of a Civ6 anthology copy (5 in total)! For all the details, please check the thread here. .
    Dismiss Notice
  2. Old World has finally been released on GOG and Steam, besides also being available in the Epic store . Come to our Old World forum and discuss with us!
    Dismiss Notice

[dev] Architecture Thread

Discussion in 'Civ3 Future Development' started by WildWeazel, Nov 22, 2021.

  1. WildWeazel

    WildWeazel Carthago Creanda Est

    Joined:
    Jul 14, 2003
    Messages:
    7,206
    Location:
    %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: Nov 23, 2021
    Civinator likes this.
  2. WildWeazel

    WildWeazel Carthago Creanda Est

    Joined:
    Jul 14, 2003
    Messages:
    7,206
    Location:
    %CIV3%\Conquests\Scenarios\
    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
     
  3. Quintillus

    Quintillus Archiving Civ3 Content Supporter

    Joined:
    Mar 17, 2007
    Messages:
    7,166
    Location:
    Ohio
    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?
     
  4. Puppeteer

    Puppeteer Emperor

    Joined:
    Oct 4, 2003
    Messages:
    1,687
    Location:
    Silverdale, WA, USA
    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.)
     
  5. WildWeazel

    WildWeazel Carthago Creanda Est

    Joined:
    Jul 14, 2003
    Messages:
    7,206
    Location:
    %CIV3%\Conquests\Scenarios\
    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
    
     
  6. Quintillus

    Quintillus Archiving Civ3 Content Supporter

    Joined:
    Mar 17, 2007
    Messages:
    7,166
    Location:
    Ohio
    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.
     
  7. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    861
    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.
     
    Quintillus and Puppeteer like this.
  8. WildWeazel

    WildWeazel Carthago Creanda Est

    Joined:
    Jul 14, 2003
    Messages:
    7,206
    Location:
    %CIV3%\Conquests\Scenarios\
    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.
     
  9. Quintillus

    Quintillus Archiving Civ3 Content Supporter

    Joined:
    Mar 17, 2007
    Messages:
    7,166
    Location:
    Ohio
    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.
     
    Flintlock and Puppeteer like this.
  10. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    861
    My condolences.



    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?
     
    Quintillus likes this.
  11. Quintillus

    Quintillus Archiving Civ3 Content Supporter

    Joined:
    Mar 17, 2007
    Messages:
    7,166
    Location:
    Ohio
    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.
     
    Flintlock and Puppeteer like this.
  12. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    861
    I was wondering that too, though not enough to look it up.
    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.
     
  13. Quintillus

    Quintillus Archiving Civ3 Content Supporter

    Joined:
    Mar 17, 2007
    Messages:
    7,166
    Location:
    Ohio
    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.
     
  14. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    861
    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.
    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.
     
    Quintillus likes this.
  15. Quintillus

    Quintillus Archiving Civ3 Content Supporter

    Joined:
    Mar 17, 2007
    Messages:
    7,166
    Location:
    Ohio
    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.
     
  16. WildWeazel

    WildWeazel Carthago Creanda Est

    Joined:
    Jul 14, 2003
    Messages:
    7,206
    Location:
    %CIV3%\Conquests\Scenarios\
    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.
     
  17. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    861
    (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.
     
    Quintillus likes this.
  18. Quintillus

    Quintillus Archiving Civ3 Content Supporter

    Joined:
    Mar 17, 2007
    Messages:
    7,166
    Location:
    Ohio
    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.
     
  19. WildWeazel

    WildWeazel Carthago Creanda Est

    Joined:
    Jul 14, 2003
    Messages:
    7,206
    Location:
    %CIV3%\Conquests\Scenarios\
    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: Feb 3, 2022
  20. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    861
    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.
     

Share This Page