Amineri Posted February 19, 2016 Share Posted February 19, 2016 I've noticed that this topic hasn't been discussed much yet, and I haven't pushed it so as to let people get a better handle on how the GameState system works in XCOM 2. However, the time has come ... There is a fairly flexible and powerful method available to us that can reduce the need to override classes : COMPONENT GAMESTATE CLASSES. The basic methods involved are: XComGameState_BaseObject.AddComponentObject XComGameState_BaseObject.FindComponentObject XComGameState_BaseObject.GetParentGameState XComGameState_BaseObject.GetRootObject XComGameState_BaseObject.RemoveComponentObject XComGameState_BaseObject.GetAllComponentObjects I've put the initial three separate, because they are the most commonly used, although the others are also useful. The basic concept here is that in many cases we can use components to extend both data and code for existing gamestates, without having to override them. Not all, but certainly enough to be useful. Since multiple mods can each separately define multiple components to be attached to a parent, this improves mod interoperability (as long as we avoid name collisions). I'll go over a couple of examples which I used in the LW_OfficerPack mod (on steam named LeaderPack). ----------------------------------- Use case 1 : Extending XComGameState_Unit To store officer-related data for each unit, I created a new class : class XComGameState_Unit_LWOfficer extends XComGameState_BaseObject config(LW_OfficerPack); Note that it extends BaseObject and not Unit, because I don't need any of the data/functions from Unit -- if I need access to Unit data/methods I can use GetParentGameState to retrieve Unit. My naming convention was to include Unit to indicate the parent GameState, not that it inherits from unit. In this class I added all of my unit-specific officer-related data : var protected int OfficerRank; var protected array<int> MissionsAtRank; var protected int RankTraining; var name AbilityTrainingName; var name LastAbilityTrainedName; I also created a utility class to provide static utility methods, including a method to retrieve the officer component of a given unit : class LWOfficerUtilities extends Object config(LW_OfficerPack); // Returns the officer component attached to the supplied Unit GameState static function XComGameState_Unit_LWOfficer GetOfficerComponent(XComGameState_Unit Unit) { if (Unit != none) return XComGameState_Unit_LWOfficer(Unit.FindComponentObject(class'XComGameState_Unit_LWOfficer')); return none; } Similar methods can be built for other situations, such as : static function bool HasOfficerInSquad(optional XComGameState_HeadquartersXCom XComHQ); static function bool IsOfficer(XComGameState_Unit Unit); static function int GetNumOfficersFromList(array<XComGameState_Unit> SoldierList); The components are created in a UI class : class UIArmory_LWOfficerPromotion extends UIArmory_Promotion config(LW_OfficerPack); This is NOT an override class, but instead runs parallel to the regular UIArmory_Promotion, and is accessed by adding a new button to UIArmory_MainMenu using a UIScreenListener. The particular bits of code used to create the component are in ConfirmAbilityCallback : //Try to retrieve new OfficerComponent from Unit -- note that it may not have been created for non-officers yet OfficerState = class'LWOfficerUtilities'.static.GetOfficerComponent(Unit); if (OfficerState == none) { //first promotion, create component gamestate and attach it OfficerState = XComGameState_Unit_LWOfficer(UpdateState.CreateStateObject(class'XComGameState_Unit_LWOfficer')); OfficerState.InitComponent(); if (default.INSTANTTRAINING) { OfficerState.SetOfficerRank(1); } else { NewOfficerRank = 1; } UpdatedUnit.AddComponentObject(OfficerState); } else { //subsequent promotion, update existing component gamestate NewOfficerRank = OfficerState.GetOfficerRank() + 1; OfficerState = XComGameState_Unit_LWOfficer(UpdateState.CreateStateObject(class'XComGameState_Unit_LWOfficer', OfficerState.ObjectID)); if (default.INSTANTTRAINING) { OfficerState.SetOfficerRank(NewOfficerRank); } } I used the lazy initialization design pattern here, although that's not necessary (https://en.wikipedia.org/wiki/Lazy_initialization) Using the Unit_LWOfficer component meant that I was able to implement everything needed for the officer mod, without having to override the Unit component. ----------------------------------------- Use Case 2 : Creating Interesting Ability Effects The use of templates allows for a lot of interesting new abilities and effects to be created. However, there are a couple of limitations -- it's not easy to store persistent data per instance of an effect, and it's not easy to register a listener in all cases. For this example, I'll be looking at the FocusFire officer ability, which is an activate-able ability that grants a cumulative +X aim bonus for each unit firing at the focussed unit (i.e. +X for first ability, +2X for second, +3X for third, and so on). The issue is that the X2Effect class is not instanced for each unit using/applying the effect. It's more of a template-type class, only unique per ability. Since I wanted to code this more generally so that it could be in effect multiple times at once, I couldn't store the counter in the X2Efffect. To hold this data, I created the class : class XComGameState_Effect_FocusFire extends XComGameState_BaseObject config(LW_OfficerPack); As with the LWOfficer component, note that it extends BaseObject, and the Effect in the class name is indicating that it is intended to attach to XComGameState_Effect as its parent. The only variable it holds is : var int CumulativeAttacks; The component is created and attached to the Effect in X2Effect_FocusFire.OnEffectAdded : if (GetFocusFireComponent(NewEffectState) == none) { //create component and attach it to GameState_Effect, adding the new state object to the NewGameState container FFEffectState = XComGameState_Effect_FocusFire(NewGameState.CreateStateObject(class'XComGameState_Effect_FocusFire')); FFEffectState.InitComponent(); NewEffectState.AddComponentObject(FFEffectState); NewGameState.AddStateObject(FFEffectState); } Again I used lazy initialization to make sure I'm only creating a single instance attached to a particular GameState_Effect. The component is retrieved using a static method, this time defined in X2Effect_FocusFire (instead of a separate utility class) : static function XComGameState_Effect_FocusFire GetFocusFireComponent(XComGameState_Effect Effect) { if (Effect != none) return XComGameState_Effect_FocusFire(Effect.FindComponentObject(class'XComGameState_Effect_FocusFire')); return none; } The CumulativeAttacks value is used in X2Effect_FocusFire.GetToHitAsTargetModifiers : FFEffect = GetFocusFireComponent(EffectState); if (FFEffect != none) { AccuracyInfo.Value = default.AIMBONUSPERATTACK * Max(1, FFEffect.CumulativeAttacks); } else { AccuracyInfo.Value = default.AIMBONUSPERATTACK; } Updating the counter was a bit trickier, because there are 3+ units involved -- Unit A uses FocusFire on Unit B, so Unit B gets the Effect applied to it (and Unit A is the Source). However, it also applies when Unit C or Unit D uses an aim-related ability on Unit B. To implement updating of the counter, I used an listener from X2EventManager. This code is in X2Effect_FocusFire.OnEffectAdded, just after the effect component is created : //add listener to new component effect -- do it here because the RegisterForEvents call happens before OnEffectAdded, so component doesn't yet exist ListenerObj = FFEffectState; if (ListenerObj == none) { `Redscreen("FocusFire: Failed to find FocusFire Component when registering listener"); return; } EventMgr.RegisterForEvent(ListenerObj, 'AbilityActivated', FFEffectState.FocusFireCheck, ELD_OnStateSubmitted,,,true); This registers XComGameState_Effect.FocusFireCheck in the instance of the effect component we are interested in, to trigger whenever 'AbilityActivated' TriggerEvent occurs. The event listener FocusFireCheck is a bit long, so I'm wrapping it in spoiler tags : simulated function EventListenerReturn FocusFireCheck(Object EventData, Object EventSource, XComGameState GameState, Name EventID) { local XComGameState NewGameState; local XComGameState_Effect EffectState; local XComGameState_Effect_FocusFire CurrentFFEffect, UpdatedFFEffect; local XComGameState_Unit AttackingUnit, AbilityTargetUnit, FocussedUnit; local XComGameStateHistory History; local XComGameState_Ability AbilityState; local XComGameStateContext_Ability AbilityContext; local XComGameState_Item SourceWeapon; //local bool bValidTarget; AbilityState = XComGameState_Ability (EventData); if (AbilityState == none) { `RedScreen("FocusFireCheck: no ability"); return ELR_NoInterrupt; } AbilityContext = XComGameStateContext_Ability(GameState.GetContext()); if (AbilityContext == none) { `RedScreen("FocusFireCheck: no context"); return ELR_NoInterrupt; } // non-pre emptive, so don't process during the interrupt step if (AbilityContext.InterruptionStatus == eInterruptionStatus_Interrupt) return ELR_NoInterrupt; SourceWeapon = AbilityState.GetSourceWeapon(); if ((SourceWeapon == none) || (class'X2Effect_FocusFire'.default.VALIDWEAPONCATEGORIES.Find(SourceWeapon.GetWeaponCategory()) == -1)) { //`RedScreen("FocusFireCheck: no or invalid weapon"); return ELR_NoInterrupt; } EffectState = GetOwningEffect(); if (EffectState == none) { `Redscreen("GameState_Effect_FocusFire: Unable to find owning GameState_Effect\n" $ GetScriptTrace()); return ELR_NoInterrupt; } History = `XCOMHISTORY; AbilityTargetUnit = XComGameState_Unit(GameState.GetGameStateForObjectID(AbilityContext.InputContext.PrimaryTarget.ObjectID)); if (AbilityTargetUnit == none) AbilityTargetUnit = XComGameState_Unit(History.GetGameStateForObjectID(AbilityContext.InputContext.PrimaryTarget.ObjectID)); AttackingUnit = XComGameState_Unit(EventSource); FocussedUnit = XComGameState_Unit(GameState.GetGameStateForObjectID(EffectState.ApplyEffectParameters.TargetStateObjectRef.ObjectID)); if (FocussedUnit == none) FocussedUnit = XComGameState_Unit(History.GetGameStateForObjectID(EffectState.ApplyEffectParameters.TargetStateObjectRef.ObjectID)); if (AttackingUnit == none) { `RedScreen("FocusFireCheck: no attacking unit"); return ELR_NoInterrupt; } if (AbilityTargetUnit == none) { `RedScreen("FocusFireCheck: no target unit"); return ELR_NoInterrupt; } if (FocussedUnit == none) { //`RedScreen("FocusFireCheck: no focussed unit"); return ELR_NoInterrupt; } if (FocussedUnit != AbilityTargetUnit) { //`RedScreen("FocusFireCheck: focussed unit != target unit"); return ELR_NoInterrupt; } CurrentFFEffect = XComGameState_Effect_FocusFire(History.GetGameStateForObjectID(ObjectID)); //`Log("FocusFireCheck: Previous CumulativeAttacks=" $ CurrentFFEffect.CumulativeAttacks); //`Log("FocusFire: 'AbilityActivated' event triggered, gates passed"); NewGameState = class'XComGameStateContext_ChangeContainer'.static.CreateChangeState(string(GetFuncName())); UpdatedFFEffect = XComGameState_Effect_FocusFire(NewGameState.CreateStateObject(class'XComGameState_Effect_FocusFire', ObjectID)); UpdatedFFEffect.CumulativeAttacks = CurrentFFEffect.CumulativeAttacks+1; NewGameState.AddStateObject(UpdatedFFEffect); `TACTICALRULES.SubmitGameState(NewGameState); return ELR_NoInterrupt; } The code has to do filtering, since it receiving every 'AbilityActivated' event, so it checks that all of the proper conditions are met. If they are, then it creates a ChangeContainer, creates an update version of the effect component, increments the CumulativeAttacks value, and submits the update gamestate. Note that effect components are NOT used by Firaxis for this -- instead all event listeners and effect-specific data is put into the XComGameState_Effect, which isn't very modular, and doesn't work for modding very well, since only one mod could override it. Using a gamestate component, the FocusFire effect is able to store persistent data and invoke a listener to update the persistent data, without using any class overrides that would conflict with other mods. --------------------- IMPORTANT : Further information on cleaning up components when the parent is removed : http://forums.nexusmods.com/index.php?/topic/3853330-psa-you-have-to-garbage-collect-your-components-yourself/ --------------------- Hopefully this guide is useful to some of you aspiring modders out there. Please feel free to ask questions about this, or to comment, etc. Link to comment Share on other sites More sharing options...
TeamDragonpunk Posted February 19, 2016 Share Posted February 19, 2016 Amazing! Thank you! Getting into the weeds of this is actually my weekend project, and you just helped considerably! What would we ever do without you?! Link to comment Share on other sites More sharing options...
Deleted32045420User Posted February 19, 2016 Share Posted February 19, 2016 hmmm seems interesting, i'll have to look if that'll help me detect HP changes Link to comment Share on other sites More sharing options...
kosmo111 Posted February 19, 2016 Share Posted February 19, 2016 Thanks for the tutorial. I actually have been playing around with this already and have a component GameState added to the Unit GameState much like you describe here. One thing that I think it might also be nice to go over is how to officially add/update these GameStates from the History object-- as that is something I am still not totally clear on. There seem to be a number of locations where you can 'submit' a GameState and the rules for which GameStates (pre-existing, modified, components) you need to add is also a little vague. For example, in the Officer mod UIArmor_LWOfficerPromotion script you use `GAMERULES.SubmitGameState. However you also seem to be able to do this via `TACTICALRULES and `HISTORY.AddGameStateToHistory. For my mod, the only one of these that ever seemed to work was `HISTORY.AddGameStateToHistory I don't suppose you could go into a bit more detail as to the differences of these techniques? Thanks! Link to comment Share on other sites More sharing options...
davidlallen Posted February 19, 2016 Share Posted February 19, 2016 This is great for adding new stuff. A similar guide for changing existing behavior or deleting existing behavior, without the "nuclear option" of replacing a whole class, would be great ... er. Link to comment Share on other sites More sharing options...
SteelRook Posted February 19, 2016 Share Posted February 19, 2016 I have a question: You speak of "components," which is a term I've never heard of before (in relation to programming, I know what the English word means). This struck me as an UnrealScript-specific term, so I went looking and turned up this writeup. Is what you're describing related to this interpretation of "components" in some way? I ask, because I'm completely lost to what the term actually means, since it's something I've never run across in OOP before. This seems like you're creating a graph of interlinked classes with some explicit ruleset designed for how they cross-reference each other, but I'm guessing at this point. Link to comment Share on other sites More sharing options...
kosmo111 Posted February 19, 2016 Share Posted February 19, 2016 Components are a way to add generic data to an object. It isn't strictly speaking an OOP design pattern. For example you might have a base Actor object. You could then add a DrawComponent and a MovementComponent to that Actor. The system could then, while drawing, look for all Actors that have a DrawComponent and give them a chance to draw. Then do the same for Movement. Take a look at https://en.wikipedia.org/wiki/Entity_component_system for more information. Link to comment Share on other sites More sharing options...
davidlallen Posted February 19, 2016 Share Posted February 19, 2016 (edited) I understood this usage of component in a much more generic way:https://en.wikipedia.org/wiki/Component-based_software_engineering#Software_componentPersonally I think of what she described as a "void *" structure added into an existing structure. That is, there is an original object which knows nothing about officers, and she has added a new user defined object as a field in the original object. The original object just knows this is a thing to be returned when requested, but doesn't know anything else about it. This is perfect for adding bags of data onto existing bags without the existing ones caring much. EDIT: kosmo111 types faster than me. Edited February 19, 2016 by davidlallen Link to comment Share on other sites More sharing options...
Amineri Posted February 19, 2016 Author Share Posted February 19, 2016 Thanks for the tutorial. I actually have been playing around with this already and have a component GameState added to the Unit GameState much like you describe here. One thing that I think it might also be nice to go over is how to officially add/update these GameStates from the History object-- as that is something I am still not totally clear on. There seem to be a number of locations where you can 'submit' a GameState and the rules for which GameStates (pre-existing, modified, components) you need to add is also a little vague. For example, in the Officer mod UIArmor_LWOfficerPromotion script you use `GAMERULES.SubmitGameState. However you also seem to be able to do this via `TACTICALRULES and `HISTORY.AddGameStateToHistory. For my mod, the only one of these that ever seemed to work was `HISTORY.AddGameStateToHistory I don't suppose you could go into a bit more detail as to the differences of these techniques? Thanks! Unfortunately, I'm also a little unclear on when each of the macros applies, and when it doesn't. I copy/paste a lot of Firaxis code, and there are a variety of methods for accessing History for submitting gamestates. `TACTICALRULES.SubmitGameState really only seems to work when in a tactical mission, which makes sense give the name.`GAMERULES.SubmitGameState seems to work in almost every situation, except.... when adding things during a OnLoadedSave in X2DownloadableContentInfo, neither of these macros work, and the only way I found to submit (copied from the ExampleWeapon) is using :`XCOMHISTORY.AddGameStateToHistory Link to comment Share on other sites More sharing options...
Amineri Posted February 19, 2016 Author Share Posted February 19, 2016 This is great for adding new stuff. A similar guide for changing existing behavior or deleting existing behavior, without the "nuclear option" of replacing a whole class, would be great ... er. I know it's not a 100% solution -- it doesn't work for stuff like trying to change out bits of X2AbilityToHitCalc in a friendly way. It's more like a 30% or 40% solution, but it's still useful in a lot of cases. Link to comment Share on other sites More sharing options...
Recommended Posts