Skip to content

Understanding Actions & Player State Actions

Where an entity encompasses a controller, model ( Entity Data), and optional state ( Saved Data), Actions and State Actions provide a way to dispatch important changes or uses of these entities or their States. Listeners, such as UI Elements or a Game Controller, can then stay up to date with and react to these Actions.

Note

It is recommended that you read the rules of using Actions and State Actions in their API summaries.

Types of Actions

There are two types of Actions in IdleKit: the regular IAction and the ISavedData altering IStateAction that is based on the former.

IAction

An IAction can be sent and subscribed to from any IEntity, and IService with a reference to IActionService. It has the following properties:

  • An Action does not alter saved data, use an IStateAction if saved data is going to be altered
  • An Action performs no direct operations
  • An Action can contain optional parameters such as the caller
  • An Action is only dispatched once, and is fired and forgotten
  • An Action is registered with the IContainer
  • An Action can then be obtained from the IActionService
  • An Action can be dispatched globally or to a group of listeners that are only interested in a specific instance

IStateAction

An IStateAction is a specialized version of an IAction. * A State Action contains all properties of an IAction with the exception that a State Action alters ISavedData * A State Action performs an Apply() direct operation that may involve different Services and Entities

All Actions that affect the player's persistent data should be done from within an IStateAction. This is to allow for the possibility of sending the Actions to a remote server for validation.

Creating an Action

Actions act as little more than information that is dispatched to subscribers. When creating an Action, you will generally only have to implement the IAction definitions and any relevant caches.

For full class references from this example, see EntityAction.cs, CurrencyAction.cs and Currency.cs from idlekit-gameplay and Context.cs from idlekit-core. We see the EntityAction implements the two requirements of its interface alongside its valuable data:

The below defines a cache field for the required Type[] getter. These are the types of listeners the IActionService will dispatch to when this Action fires. Derivatives of this abstract Action class will populate the field in the constructor.

protected Type[] _typesToDispatchAs;
public virtual Type[] TypesToDispatchAs => _typesToDispatchAs;

While derivatives may contain more data, our abstract EntityAction has its required data reference(s).

public IEntity Entity { get; set; }

Because all Actions are bound as singleton, it is a requirement to implement a Reset method that clears any state held within the Action.

public virtual void ResetAction()
{
    Entity = null;
}

Moving on to an implementation in the CurrencyAction.cs file, CurrencyChangedAction, we see it utilizes these abstract parts to function as a unique Action:

Here, we populate our dispatch cache to define which listener types should be invoked by this Action's dispatching.

public CurrencyChangedAction()
{
    _typesToDispatchAs = new []{ typeof(CurrencyChangedAction), typeof(CurrencyAction), typeof(EntityAction) };
}

The base data is still relevant, but handling a change in our cached entity (Currency) required a bit more data to make this Action useful to its listeners.

public double OldValue { get; set; }
public double NewValue { get; set; }
public double Difference { get; set; }

With more data comes more responsibilities of the ResetAction() method. Resetting our values ensures no residual data gets used incorrectly in future uses of this IAction.

public override void ResetAction()
{
    base.ResetAction();
    OldValue = 0;
    NewValue = 0;
    Difference = 0;
}

Using Actions

Now that we have an Action, we want to do three things: bind it, fire it, and handle it.

For full class references from this example, see IKInstaller.cs, Currency.cs, CurrencyAction.cs and CollectGoal.cs from idlekit-gameplay.

Binding

Actions are bound in the IContext within the BindActions call. In the following code from the Context class, we bind a delegate and type to the Container. Because the Actions are bound as singletons, only one instance will be created the first time the Action is resolved for use.

 public virtual void BindActions(IContainer container)
        {
            // StateActions
            container.Bind<AdvanceStageStateAction>().AsSingleton().Conclude();
            container.Bind<AscensionCompleteStateAction>().AsSingleton().Conclude();
            container.Bind<AutomateGeneratorStateAction>().AsSingleton().Conclude();
            container.Bind<BuyFromStoreCollectionStateAction>().AsSingleton().Conclude();
            container.Bind<BuyCollectorStateAction>().AsSingleton().Conclude();
            container.Bind<BuyGeneratorStateAction>().AsSingleton().Conclude();
            container.Bind<BuyStoreRewardStateAction>().AsSingleton().Conclude();
            container.Bind<ClaimGoalStateAction>().AsSingleton().Conclude();
            container.Bind<ClaimMilestoneTrackRewardsStateAction>().AsSingleton().Conclude();
            container.Bind<ClaimStoreTimedRewardStateAction>().AsSingleton().Conclude();
            container.Bind<ClaimTimedTrackRewardsStateAction>().AsSingleton().Conclude();
            container.Bind<ClaimEventRewardsStateAction>().AsSingleton().Conclude();
            container.Bind<ClaimEventRankRewardsStateAction>().AsSingleton().Conclude();
            container.Bind<ClearContentIdStateAction>().AsSingleton().Conclude();
            container.Bind<ClearEventsSavedDataStateAction>().AsSingleton().Conclude();
            container.Bind<CollectFromCollectorStateAction>().AsSingleton().Conclude();
            container.Bind<CollectFromGeneratorStateAction>().AsSingleton().Conclude();
            container.Bind<CompleteMilestoneTrackStateAction>().AsSingleton().Conclude();
            container.Bind<ProgressTrackStateAction>().AsSingleton().Conclude();
            container.Bind<CreateGoalStateAction>().AsSingleton().Conclude();
            container.Bind<EventEndedStateAction>().AsSingleton().Conclude();
            container.Bind<EventStartedStateAction>().AsSingleton().Conclude();
            container.Bind<GeneratorUnitTargetHitStateAction>().AsSingleton().Conclude();
            container.Bind<IncrementGeneratorUnitStateAction>().AsSingleton().Conclude();
            container.Bind<LogLastActiveTimeStateAction>().AsSingleton().Conclude();
            container.Bind<MakeExchangeStateAction>().AsSingleton().Conclude();
            container.Bind<NewContentStateAction>().AsSingleton().Conclude();
            container.Bind<NewStageStateAction>().AsSingleton().Conclude();
            container.Bind<NewUserStateAction>().AsSingleton().Conclude();
            container.Bind<ProgressGoalStateAction>().AsSingleton().Conclude();
            container.Bind<ProgressMilestoneStateAction>().AsSingleton().Conclude();
            container.Bind<ResetEntitySavedDataStateAction>().AsSingleton().Conclude();
            container.Bind<SetAvailableCurrenciesStateAction>().AsSingleton().Conclude();
            container.Bind<SetContentStateAction>().AsSingleton().Conclude();
            container.Bind<SetEventEndTimeStateAction>().AsSingleton().Conclude();
            container.Bind<SetGoalProgressStateAction>().AsSingleton().Conclude();
            container.Bind<SetMilestoneProgressStateAction>().AsSingleton().Conclude();
            container.Bind<SetMilestoneCompletionInfoStateAction>().AsSingleton().Conclude();
            container.Bind<StoreTimedRewardInitializeStateAction>().AsSingleton().Conclude();
            container.Bind<StoreTimedRewardUnlockedStateAction>().AsSingleton().Conclude();
            container.Bind<ToggleBoostStateAction>().AsSingleton().Conclude();
            container.Bind<ToggleMilestoneStateAction>().AsSingleton().Conclude();
            container.Bind<ToggleTimedActivatableStateAction>().AsSingleton().Conclude();
            container.Bind<ToggleTimedBoostStateAction>().AsSingleton().Conclude();
            container.Bind<ToggleTrackStateAction>().AsSingleton().Conclude();
            container.Bind<ToggleAvailableStateAction>().AsSingleton().Conclude();
            container.Bind<UpgradeCurrencyStateAction>().AsSingleton().Conclude();
            container.Bind<TogglePromoReadyStateAction>().AsSingleton().Conclude();
            container.Bind<ToggleTimeLeftInEventTriggeredStateAction>().AsSingleton().Conclude();
            container.Bind<ClaimNumGoalsTriggerProgressStateAction>().AsSingleton().Conclude();
            container.Bind<MilestonesCompletedTriggerProgressStateAction>().AsSingleton().Conclude();
            container.Bind<EventPhaseStartedStateAction>().AsSingleton().Conclude();

            // Actions
            container.Bind<StageAdvancedAction>().AsSingleton().Conclude();
            container.Bind<AscensionAvailableAction>().AsSingleton().Conclude();
            container.Bind<AscensionStartAction>().AsSingleton().Conclude();
            container.Bind<ClearContentAction>().AsSingleton().Conclude();
            container.Bind<CollectorInitializedAction>().AsSingleton().Conclude();
            container.Bind<CollectorPayoutChangedAction>().AsSingleton().Conclude();
            container.Bind<CollectorSpeedChangedAction>().AsSingleton().Conclude();
            container.Bind<CollectorStateChangedAction>().AsSingleton().Conclude();
            container.Bind<ContentInitializedAction>().AsSingleton().Conclude();
            container.Bind<CurrencyAvailableAction>().AsSingleton().Conclude();
            // <bindAnAction>
            container.Bind<CurrencyChangedAction>().AsSingleton().Conclude();
            // </bindAnAction>
            container.Bind<CurrencyObtainedAction>().AsSingleton().Conclude();
            container.Bind<EntityAddedAction>().AsSingleton().Conclude();
            container.Bind<EntityInitializedAction>().AsSingleton().Conclude();
            container.Bind<EntityRemovedAction>().AsSingleton().Conclude();
            container.Bind<GeneratorInitializedAction>().AsSingleton().Conclude();
            container.Bind<GeneratorPayoutChangedAction>().AsSingleton().Conclude();
            container.Bind<GeneratorSpeedChangedAction>().AsSingleton().Conclude();
            container.Bind<GeneratorStateChangedAction>().AsSingleton().Conclude();
            container.Bind<GeneratorModifierIndexChangedAction>().AsSingleton().Conclude();
            container.Bind<GoalActivatedAction>().AsSingleton().Conclude();
            container.Bind<IdleKitInitializedAction>().AsSingleton().Conclude();
            container.Bind<LocalizationLanguageChangedAction>().AsSingleton().Conclude();
            container.Bind<MilestoneClaimableAction>().AsSingleton().Conclude();
            container.Bind<ModifierRegisteredAction>().AsSingleton().Conclude();
            container.Bind<ModifierUnregisteredAction>().AsSingleton().Conclude();
            container.Bind<ModifierIndexChangedAction>().AsSingleton().Conclude();
            container.Bind<ModifierToggledAction>().AsSingleton().Conclude();
            container.Bind<OfflineProgressStartAction>().AsSingleton().Conclude();
            container.Bind<OfflineProgressEndAction>().AsSingleton().Conclude();
            container.Bind<PostSerializationAction>().AsSingleton().Conclude();
            container.Bind<PreClearContentAction>().AsSingleton().Conclude();
            container.Bind<PreSerializationAction>().AsSingleton().Conclude();
            container.Bind<RewardGrantedAction>().AsSingleton().Conclude();
            container.Bind<StageInitializedAction>().AsSingleton().Conclude();
            container.Bind<StoreCollectionRefreshedStateAction>().AsSingleton().Conclude();
            container.Bind<TimedActivatableEndedAction>().AsSingleton().Conclude();
            container.Bind<StoreTimedRewardAvailableAction>().AsSingleton().Conclude();
            container.Bind<StoreTimedRewardStartAction>().AsSingleton().Conclude();
            container.Bind<TrackCompletedAction>().AsSingleton().Conclude();
            container.Bind<TradeAcceptedAction>().AsSingleton().Conclude();
            container.Bind<TradeDeclinedAction>().AsSingleton().Conclude();
            container.Bind<TriggerCompletedAction>().AsSingleton().Conclude();

            container.Bind<SuccessfulSignInAction>().AsSingleton().Conclude();
            container.Bind<FailedSignInAction>().AsSingleton().Conclude();
            container.Bind<RequestSignInAction>().AsSingleton().Conclude();
            container.Bind<SuccessfulRegistrationAction>().AsSingleton().Conclude();
            container.Bind<FailedRegistrationAction>().AsSingleton().Conclude();
        }
var action = new Action();
action.Fire();

Firing

Actions are fired by fetching a reference to the type of Action, initializing that reference, and then dispatching that reference. All of this is done through the IActionService.

The first line of code below retrieves the Action that was bound to the container. The last line dispatches the Action to inform any listeners. Everything in between is just filling in valuable information the Action may carry to a listener.

Because this Action pertains to a specific object, we fill in the optional parameter in `Dispatch`1(0,System.Object). We see how this relates to the listener in the next snippet.

 protected virtual void SendCurrencyChangedAction(double oldValue, double newValue, double difference)
        {
            CurrencyChangedAction currencyChangedAction = _actionService.Get<CurrencyChangedAction>();
            currencyChangedAction.Currency = this;
            currencyChangedAction.OldValue = oldValue;
            currencyChangedAction.NewValue = newValue;
            currencyChangedAction.Difference = difference;

            _actionService.Dispatch(currencyChangedAction, this);
        }

Handling

Now that we're firing this Action, we want to handle it. It could be to update our UI or even inform other pieces of logic within the project. In this case, we have a IGoal (CollectGoal from idlekit-gameplay) that requires the player to collect a certain amount of ICurrency which is a perfect case for our CurrencyChangedAction.

In our first function, we see the goal subscribe to our CurrencyChangedAction with a handler and the optional target. Filling this optional parameter means that this listener will be invoked if the dispatching of this Action has the same "target" (subject, cause, etc) object reference.

In this case, it means we will only track the specific ICurrency that our goal is tracking and, by doing this, we increase efficiency by only listening to this Action if it's caused by the currency we care about. Without this optional parameter, we subscribe to every dispatching of this Action.

protected override void SubscribeActionListener(IEntity target)
        {
            _actionService.Subscribe<CurrencyChangedAction>(OnCurrencyChanged, target);
        }

Note

The Subscribe<T>(Action<T>, Int32) method and its overloads contain an optional Int32 parameter where the subscriber can specify the priority that its listener is invoked in. This is useful in the case that there are multiple listeners subscribed to the same action. Global subscribers (ie. those subscribed without an instance) are always invoked prior to those that subscribe with an instance.

After subscribing, the method passed in is now handling the Action dispatches. Here, we use data from the Action to determine a potential early out, as well as how much further along the goal will progress otherwise.

  protected virtual void OnCurrencyChanged(CurrencyChangedAction currencyAction)
        {
            if (currencyAction.Difference <= 0)
            {
                return;
            }

            ProgressGoal(currencyAction.Difference);
        }

Every subscription should be unsubscribed, usually when the Action no longer requires handling or the listening object is going out of scope. Below is an example of unsubscribing from the previously subscribed Action.

protected override void UnsubscribeActionListener(IEntity target)
        {
            _actionService.Unsubscribe<CurrencyChangedAction>(OnCurrencyChanged, target);
        }