Sci-Fi Shooter Demo

v1.0

The Sci-Fi Shooter Demo demonstrates how to pass data along from one object to another via a projectile that utilizes Juicy Actions. We also add game feel / juice via many actions on the characters, the audio, the camera, and post processing effects.

We also add juice and animation to the UI elements through the scene.

Player

First look at the Player object in the scene. There are more traditional classes for movement (SciFiDemoMovement), and handling the direction that the player is facing (TargetDirection), based on where the mouse is. In this game, the player movement is independent from the direction they are facing.

Firing Bullets

We utilize the ActionOnMouse component for firing. The onButtonStay flag is true, so the OnMouseActions ActionExecutor will be engaged whenever the left mouse button is held down. This executor can restart, with a cooldown of 0.2 seconds. The cooldown is what controls the firing rate for this demo.

The SpawnAction is used, spawning a Bullet prefab. Note that the rotationMode value is set to Spawn Transform Forward, so that the bullet will be facing the same direction as the spawner transform, in this case, the player which is the Target object of the ActionExecutor.

We are also using the built-in Object Pool, and the autoReleaseTime is set to 20 seconds — bullets spawned will return to the pool in 20 seconds.

Player Class

The SciFiDemoPlayer holds the players health, plus three ActionExecutor fields for onHitActions, onHealActions, and onDeathActions.

On Hit Actions

When the player is hit, we do a number of actions at once, all to add Juice to the game.

The player sprite will flash red using ColorChangeAction, and PlayAudioClipAction will play "got hit" clip.

We send an event to the Action Blackboard with SendBlackboardEventAction which tells the camera (and other objects) to shake. We also make the camera field of view shift over a period of 0.33 seconds using the SetVirtualCameraFieldOfViewAction.

The curve animates the Field of View in and out very quickly, adding some additional game feel.

Finally, we affect the Post Processing using SetChromaticAberrationAction and SetVignetteColorAction. These are both also animated over 0.33 seconds, and return to their original position, just like the field of view.

On Heal Actions

We do very similar things for the actions that trigger when the player is healed, though with different values, and fewer actions in total.

While we are re-using the "Color Flash Red" scriptable object, it is a ColorChangeAction and we override the color to be green instead of red. We also play an audio clip — not the got hit clip, but another one, and we SetVignetteColorAction to green.

We do not shake the camera when healed.

On Death Actions

When the health goes to 0, instead of executing OnHitActions, the class will execute OnDeathActions. In this demo scene, these actions do some heavy lifting with the game logic.

The first action, TriggerUnityEventAction, calls the StopOnHitActions() method on the player SciFiDemoPlayer class. This in turn calls StopAllActions() on the onHitActions, in case they are still running from an earlier hit in quick succession.

Next, we play an "on death" audio clip using PlayAudioClipAction, while stopping the music using StopAudioSourceAction.

The TriggerUnityEvent action uses a regular Unity Event to set the Game Over Panel active — this is a UI panel with the "Game Over" text. The SetGameObjectActiveAction could be used for this purpose as well.

We also shake the camera using the SendBlackboardEventAction, update the cameras field of view with SetVirtualCameraFieldOfViewAction, and desaturate the Post Processing color using SetColorSaturationAction. Both of these actions do NOT return to default, as in this demo, death is an end-of-scene event, we reload the scene to restart the game.

Finally, we pause the Juicy Action Clock using the SetTimescaleAction.

With the effectDuration set to 0, the Juicy Actions timescale is immediately set to 0, meaning all actions that are "over time" and use this deltaTime value, will pause.

The rest of the logic is now handled by the Game Over Panel ui elements, described later in this doc.

Enemies

The Enemy objects are set up with similar actions as the player (but with different logic and juice), using an ActionExecutor field for onHitActions and onDeathActions. These are in the SciFiEnemy class.

When the enemy is hit, we play an audio clip using PlayAudioClipAction, and flash the enemy sprite red using ColorChangeAction.

Enemy Death

The onDeathActions are a bit more in-depth.

We track the number of enemies killed on the global Action Blackboard. The IncrementBlackboardValueAction will add a value of 1 to the key we set for tracking this value. The PlayAudioClipAction is the enemies "death" sound.

Spawning Drops

The enemies will drop a Medkit if the player health is less than 100, otherwise they will drop a Star, which is the point system for this demo — collect as many stars as possible.

The onDeathActions list of actions has two groups: "Spawn Health" and "Spawn Star". Each of these has a SpawnAction, with one of the two prefabs populated. Note the Star object is spawned a higher transform position offset. Both of these groups also have a Conditional active.

Both Conditionals check for the Blackboard key "Player Health". For Spawn Health, we pass if the value is less than 100. For Spawn Star, we pass if the value is greater or equal to 100.

The Spawn Health Conditionals has an additional "Chance" check. Currently it is set to 100%, but if you wanted to make the game harder, you could reduce this so fewer Medkits are spawned.

Return to Object Pool

These objects can use the Object Pool. The overrides for ReturnToPoolAction have the Enemy itself as the target GameObject. Later, the Spawner objects will pull these from the pool, if possible.

Spawners

The Spawner objects handle spawning Enemy objects. They do this using an ActionRunner component, with a WaitRandomAction set to 2-4 seconds, and a SpawnAction, with the Enemy prefab set.

Reducing the range in the WaitRandomAction would produce a more fair game with predicable results.

By default, SpawnAction will utilize the Object Pool, though you can toggle that off if you prefer. Objects will be instantiated if there are none in the pool.

Medkits

The Medkit will heal the player. Each one has a trigger Collider on it, and the ActionOnTrigger component. We use the onEnterActions.

The ActionOnTrigger component has various options you can set to reduce the number of objects which may trigger actions.

In this demo, we set the "Allowed Names" to include just the string "Player", so only an object with that exact name will trigger the Action Executor.

When onEnterActions executes, the SendBlackboardEventAction will send an event with the key "Player Damage" and a string value of "-10". Receivers can parse that into an int or float. SciFiDemoPlayer on the Player object handles event notifications.

In this method we ensure the key matches "Player Damage", and that it is an event. If the value is not an int, we return, otherwise we pass that damage value to the TakeDamage() method.

The onEnterActions then destroys the object with DestroyObjectAction. We could return this to the pool, but for the demo I wanted to showcase a variety of actions.

Stars

The goal of the game is to get as many Stars as possible. They are dropped by Enemy objects when the Player health is full. These use Juicy Actions for two purposes.

Proximity Trigger

When the Player gets close enough to the Star, the Star will move toward the player. This is done with an ActionOnTrigger component which has a TriggerUnityEventWithTriggerDataAction on it — this Action will pass the collider data that is stored in the local Blackboard to whatever method you'd like on any class.

Once the Star gets close to the Player, the ActionExecutor Reached Target Actions will execute. This does three things. First, we increment the Blackboard value with IncrementBlackboardValueAction, adding 1 to our score.

Then, we use PlayAudioClipAction to play an audio clip, and ReturnToPoolAction to return the Star to the Object Pool.

UI

There are Actions on the UI elements as well.

Start Game Button

Check the Start Game object, which is under Canvas / Main Panel / Start Game Panel. We use an ActionOnEnable component to execute actions right when the game starts.

Action On Enable

First we utilize the CanvasGroupOpacityAction and the RectTransformScaleAction to fade the button in while moving it from very large, to normal sized over a short period of time. The WaitAction is used to delay the sound ,which allows for the animations to have some time to execute. Note the previous actions are longer in duration than the WaitAction is.

Then we PlayAudioClip action before WaitAction another 0.1 seconds.

Finally, we shake the button with some force using RectTransformShakeAction. The effect is to "Drop" the button onto the screen.

Action Runner with UI Button

The Start Button also has an ActionRunner component with the auto start and auto restart both false. This is executed by the UI Button being pressed.

This is the ActionExecutor that actually starts the game. First we make the button non-interactable so users don't click it multiple times with the ButtonInteractableAction and we PlayAudioClip.

The SetTimescaleAction will set the Juicy Actions Clock timescale to 1, which allows the in-game objects and logic which utilize the clock to start moving.

We also set the CanvasGroupOpacityAction to a value of 0, over a period of 0.25 seconds. After that, we use SetGameObjectActiveAction to turn off the Start Game Panel off entirely.

Game Over Panel

When the player dies, we immediately enter the "Game Over" state by turning on the Game Over Panel UI object, which has an ActionOnEnable component.

Saving the High Score

After setting the timescale to 0 with SetTimeScaleAction, we save the high score — the action group Save High Score first checks in an Conditional the Player Prefs Value of "High Score" against the value of the Global Blackboard key "Stars".

If the current high score is less than the stars value, then we will execute the action — a simple SetPlayerPrefsAction which stores the new high score in PlayerPrefs.

Displaying End-Game Data

After a 1 second WaitAction, which allows the visual effects of the player death to complete, we turn on UI objects to display additional data with motion and juice. Note that each object which is turned on has ActionOnEnable to produce its own animations and juice.

The Game Over element is turned on with SetGameObjectActiveAction before we wait an additional 1 second with WaitAction, before we toggle on the Your Score and High Score objects.

Each of those UI text objects will count up from 0 to their final value. We start that process as soon as they are enabled using the TriggerUnityEventAction, which calls the Execute() method on their SetIncrementingNumberFromPlayerPrefs components.

After an additional WaitAction for 1 second, we toggle on the Reload Scene button using another SetGameObjectActiveAction.

We hope that through this demo you've learned a lot about how Juicy Actions can drive game logic and really compeling game feel / juice, while passing data between objects.

Demo Scenes

Last updated