Guest post by Sondre Agledahl: Microsoft Student Partner, Games programmer and CS student at UCL.
Object-orientated programming is dead! Long live ECS!
Entity-Component System is a powerful architectural pattern often used in game development. It’s in keeping with the general software architecture trend away from inheritance and object-orientated principles to a more data-driven approach to programming. ECS aims to make codebases as modular and memory-efficient as possible through a number of unique design principles, key of which is a clear separation between data and behaviour.
Unity’s recent talks
have made it clear that they are embracing ECS and slowly rolling it out in favour of its old object-component model, so it is high time to familiarise oneself with this approach to writing code.
Before Unity’s official new structure is deployed, however, you will have ample opportunity to try out this system through numerous open-source ECS frameworks compatible with Unity. This 2-part tutorial explores the Svelto.ECS framework
(actively maintained on GitHub
by creator Sebastiano Mandalà), and will help you build a complete zombie survival shooter game (pictured below) using Svelto.ECS. Through the concepts you’ll explore by implementing this game, you should gather the experience to make your own ECS-based Unity games from scratch.
(This tutorial assumes familiarity with Unity’s standard MonoBehaviour structure. If you’ve already built a couple of simple games in Unity and have passing knowledge about its key features, you should be good to go.)
ECS – A primer
The core idea of ECS is contained in
Entities
, which are actually not altogether different from the basic
GameObject
s in Unity, as they are objects that define tangible
things
inside your game. These Entities are simply containers for the
Components
that are attached to them (such as
positionComponent, healthComponent, movementComponent
, etc.). Where this structure differs from Unity’s standard models (and OOP design generally), is that neither Entities nor Components have any behaviour defined on them – they contain no class methods, only dumb data as member variables.
Rather, all the behaviour that acts on Entities and their Components is contained inside
Engines
(also known in some ECS frameworks as
Systems
). Inside Engines, all of the methods that define how certain Entities should behave are executed. In this way, rather than having each object with a movement script executing its own movement function to move, in ECS a single MovementEngine acts on all Entities with MovementComponents to create the same result.
The final key concept found in Svelto.ECS is the idea of
EntityViews
. An EntityView is simply a wrapper around each Entity that Engines use to interact with different Entities in a polymorphic way. Several of these EntityView wrappers can be defined for each Entity depending on the different roles the Entity might play in each engine.
For a tangible example, in our game, the Player Entity plays several unrelated roles: Firstly, it needs to store data about player input created by the
PlayerInput
Engine to indicate where the player is aiming their gun. It also, however, needs to store a position for the
ZombieMovement
Engine to read to cause all Zombies to move towards the player’s position. These two roles do not (and really should not) need to interact whatsoever, which is why the Player Entity has two EntityViews (EV) defined:
PlayerEV
and
ZombieDestinationEV
. These two EntityViews, despite referring to the same Entity under the hood, each contain references only to those Components that are relevant to their one purpose.
The diagram below shows the structure of our game that we will cover in this tutorial.
If all of that sounds very abstract, then let’s get started making a game and make things a little more tangible. We’ll explain concepts in more detail as they become relevant.
Shooting guns and reading input
Download
the project skeleton from GitHub
and open it in Unity. This project includes a copy of Svelto.ECS (and its utility libraries
Svelto.Tasks
and
Svelto.Common
), as well as a half-finished game structure.
All Entities, Components and Engines necessary for the game have been defined already (according to the diagram above), but it’s up to us to write the Engines’ behaviour so they do what we want them to.
(Have a look around the project to get a feel for it if you’d like. Notice that Entities themselves are defined only in tiny little class definitions (
EntityDescriptors
), whereas their respective EntityViews are what define which Components each of them actually hold. You may also notice the mysterious
Implementor
classes, which we will cover later – though the astute reader can probably gather what purpose they serve with a little nosing around. If you’re curious about how all Entities and Engines are initialised, have a peek in the
MainContext.cs
file (which is attached to the
GameContext
GameObject in the Unity scene).)
Let’s begin by taking a look at the
GunShootingEngine
(
Assets/Scripts/Engines/Player/GunShootingEngine.cs
) in Visual Studio (or any code editor of your choice). As the name implies, this class will control the shooting behaviour of the Player’s gun.
You’ll notice that
GunShootingEngine
is a
“SingleEntityViewEngine”
for the
PlayerEV
, which simply means it will receive a reference to
PlayerEV
when the game begins through its
Add
method (where you can see we’re storing the reference as a member variable for safekeeping).
GunShootingEngine
is not, however, a
MonoBehaviour
, which means it will not have any
Awake, Start
or
Update
functions called automatically by Unity as we might be used to. How, then, are we going to ensure this Engine is updated every frame? With a
coroutine
.
We’ll start by defining our coroutine in the usual way as a method that returns an IEnumerator. Every time this method updates, it will check to see whether the Player wants to fire their gun, so name it accordingly.
Inside our new coroutine, we will very simply loop forever, checking at every iteration whether the value of
isFiring
inside our
PlayerEV
’s
inputComponent
is set to
true
. Being wary of infinite loops, we remember to
yield return null
at the end of our loop to make sure it only continues its next iteration in the next frame.
If
isFiring
is indeed true, we will call a wishfully defined
Shoot
function – you can go ahead and define that now (as a simple void no-argument function), or simply let Visual Studio auto-generate it for you (move your cursor to your function call and press ALT+ENTER).
Before we implement our new function, let’s satisfy one likely point of curiosity: Where is the value of
isFiring
being set? A very good question we should address right away. Let’s hop over to
PlayerInputEngine
(
Assets/Scripts/Engines/Player/PlayerInputEngine.cs
) and make sure it’s doing what we want it to do before we proceed.
PlayerInputEngine
is a
SingleEntityViewEngine
for the
PlayerEV
as well. The purpose of this engine will be to continuously read input from the Player’s mouse and store these as values in the
PlayerEV
’s
inputComponent
.
(You can have a look at the definition of the Player’s
inputComponent
(
Assets/Scripts/Components/Player/PlayerInputComponent.cs
) to get a sense of the values this engine will modify.)
Let’s make a coroutine to do this input reading; as per usual, we’ll want an infinite loop inside it that yields at the end of each iteration.
There are three
inputComponent
values that we want to update here:
aimPos
(which is the screen space position of the mouse),
isFiring
(which is a boolean indicating whether the Player clicked their primary mouse button this frame) and
aimRay
(which will be a Unity Ray that points straight forward from the Player’s mouse position into world space).
For the first two, we’ll simply take Unity’s
Input.Position
and
Input.GetButtonDown(“Fire1”)
values and store them as-is. Then we’ll use a function of our member variable
m_Camera
(which is simply a wrapper around Unity’s camera utility functions),
ScreenPointToRay
, to convert the Player’s mouse position into a Ray for us to store.
Finally, we’ll call our coroutine as soon as the Engine boots up inside the
Add
function. Again, since our Engines are not
MonoBehaviour
s, we don’t have access to Unity’s
StartCoroutine
function, but the Svelto.ECS utility library Svelto.Tasks lets us do exactly the same thing (in fact, even more efficiently!) by simply typing
ReadInput().Run()
.
With Player Input neatly sorted, let’s return to
GunShootingEngine
to complete it. now that we know we’re reading accurate input values.
Inside our
Shoot
function in
GunShootingEngine
, we want to do three things: Check whether the Ray from the Player’s mouse is touching a Collider object, check whether that object is a Zombie, and – if it is, decrement its health. We’ll also want to store a reference to the point of impact in a
Vector3
for reasons that will become clear presently. Feel free to copy the code below, or implement it gradually as we go through it.
The
m_RayCaster
member object is just another wrapper around Unity’s RayCasting functionality, and its function
GetRayHitTarget
will return the Entity ID of the first Entity that was hit by the Player’s Ray, or -1 if nothing was hit.
If we did in fact hit something, we make use of our
IEntityViewsDatabase
property, which (as the name implies) is a database of all the EntityViews currently alive in the scene.
(Where did this property magically appear from? Notice that
GunShootingEngine
implements the eloquently named
IQueryingEntityViewEngine
interface. All engines that implement this interface will automatically have a reference to this database assigned to them when Svelto.ECS initialises. It will quickly become apparent that these databases are crucial for engines to function properly.)
With this EntityView database, we check to see whether the Entity we hit is a
GunTargetEV
using the
TryQueryEntityView
function.
(Recall from the diagram earlier that
GunTargetEV
is really just a synonym for the Zombie Entity. (Or, more accurately,
GunTargetEV
is one of the EntityViews that define what a Zombie is.))
If it is, we’ll access that GunTarget’s
healthComponent
, and decrease its
currentHealth
value by the
damagePerShot
value defined in our
PlayerEV’s
gunComponent
(this is just a simple
int
value currently set to 1 that we can customise later). Finally, we’ll store the position of our
impactPoint
(the point where our bullet touched the Zombie) in the
lastImpactPos
value of our
PlayerEV’s gunComponent
.
Remembering to start our
CheckForFire
coroutine right after our
PlayerEV
reference has been assigned, we now have functioning gun shooting behaviour in our game. With a little bit of sound and visual effects we’ll be able to see the impact of this Engine.
Now, if you diligently copied the lines of code into
Shoot
from above, you might still be curious why the two variables we assigned to at the end of
Shoot
have a
.value
property attached at the end, and what this could be useful for. Rightfully so – let’s explore that right away.
Broadcasting messages and splattering blood
If you take a look at the definition of
PlayerEV’s gunComponent
(
Assets/Scripts/Components/Player/GunComponent.cs
), you’ll notice that
lastImpactPos
is not just a regular
Vector3
, but actually a
“DispatchOnSet<Vector3>”
.
This
DispatchOnSet
data type actually represents one of the primary means of engines communicating with each other in Svelto.ECS.
(There are actually several useful methods for communicating across classes in Svelto that are equally modular (and I would encourage you to try them out!), but for the sake of simplicity we will only use
DispatchOnSet
in this tutorial).
DispatchOnSet
is a wrapper around a simple value-type variable, but is also a self-contained Observer-Listener object. Any class that can access a
DispatchOnSet
property can sign up callback functions to be notified whenever the value it refers to is changed.
So, in the case of our
lastImpactPos
variable, the moment we changed its value inside
GunShootingEngine
, every class that signed up to listen for that change would be immediately notified. At the moment, however,
lastImpactPos
has no subscribers, so let’s do something about that.
Navigate to the
GunEffectsEngine
(
Assets/Engines/Player/GunEffectsEngine.cs
). This engine, too, works on the single EntityView
PlayerEV
. Inside here we’re going to instantiate a simple blood splattering particle effect whenever the player’s gun successfully shoots a zombie.
Inside the Add function, right after we’ve been assigned a reference to
PlayerEV
, we’ll sign up to be notified of changes to
lastImpactPos
. To do this, simply access
PlayerEV
’s
gunComponent
, and call
lastImpactPos’s
NotifyOnValueSet
function, passing in a wishfully titled callback function name.
Again, you can let Visual Studio auto-generate an appropriate function for this purpose (simply select the function name you passed in and hit ALT+ENTER).
As you’ll note from Visual Studio’s auto-generated parameters (which we can rename once we know what they refer to), this function will take in an
int
, which is the ID of the Entity that holds the value that was just changed, and the changed value itself (in this case, a
Vector3
).
If you recall from your look at the
gunComponent
earlier, it already contains a
gunEffectPrefab
variable (where this has been assigned we will cover in just a moment). Let’s retrieve that and instantiate it at the gun’s impact position.
We instantiate our prefab using the
Build
function of our member object
m_GameObjectFactory
.
(
GameObjectFactory
is (for our purposes, anyway) little more than a wrapper around Unity’s familiar
Instantiate
function that is passed in manually through
GunEffectsEngine
’s constructor
– check out
MainContext.cs
to see where the construction of this object happens.)
With the prefab instantiated, we assign its position to the new bullet impact position that we just had passed in.
With that, we now have bullets in the game that splatter blood particles on impact (you can check and tweak the blood effect by inspecting the prefab (
Assets/Prefabs/Blood.prefab
)). Of course, until we have Zombies spawning, we have little to test this functionality against.
Before we proceed with developing our Zombie-spawning functionality, however, let’s take a moment to unwrap the final key piece of Svelto.ECS that we’ve only skimmed over up to this point:
Implementors
.
So far, we’ve accessed Components belonging to several different Entities, using them both to read constant data (such as
damagePerShot
and
bloodEffectPrefab
), and to edit data for other Engines to read (such as
currentHealth
and
lastImpactPos
).
If you’ve inspected the definitions of these Components, you’ll have seen that they are simply interfaces that declare the existence of these properties. This is another deliberate design decision to keep Svelto.ECS codebases as modular as possible, but it raises the immediate practical question – where are they implemented? Investigate the
Assets/Scripts/Implementors
folder (and its subfolders) to get a clear answer.
These
Implementor
classes provide specific class implementations of every Component in the game. While some of them (notably
PlayerInputImplementor
) are very straightforward classes that simply define the properties laid out in their interface, others (
GunImplementor
, and several of the Zombie Implementors which we’ll address in a moment) are subclasses of
MonoBehaviour
as well, and contain some interaction with Unity.
Implementors are where these traditional Unity interactions can take place (such as receiving a
OnTriggerEnter
callback or receiving a
SerializeField
property from the Unity Inspector).
(Notice, for example, that the
bloodEffectPrefab
we accessed in
GunEffectsEngine
is simply a serialised GameObject variable that has been dragged in from the
Assets/Prefabs
folder onto the
GunImplementor
MonoBehaviour script attached to the
Player Camera
object in our Unity scene.)
Implementors, in other words, are the bridge between Unity and Svelto.ECS, allowing the rest of the codebase to be (mostly) independent from how Unity works.
With that little aside, let’s return to our Engine development, with a mind towards getting Zombies spawning in the game.
Spawning zombies and making them move
If you’ve looked around our Unity scene, you’ll have seen there’s a
ZombieSpawner
object with a number of empty child transforms called
SpawnPosition
. These are the places we’ll want our Zombies to spawn from.
Open the
ZombieSpawnerEngine
(
Assets/Scripts/Engines/Zombies/ZombieSpawnerEngine.cs
), and you’ll the same basic structure as the other engines we’ve looked at.
(You can inspect the definition of the ZombieSpawnerEV, and its
zombieSpawnerComponent
to get an idea of the data we’ll be retrieving and manipulating here).
The purpose of this engine is very simply to spawn a new zombie entity every few seconds. Let’s create another coroutine to do this.
Inside this coroutine we’re going to loop infinitely, at every iteration picking a random spawn position from the spawnPosition field of our
ZombieSpawnerEV
’s spawnerComponent. Finally we’ll build that same component’s
zombieToSpawn
prefab similar to how we’ve done before, and place it at the randomly chosen spawn position.
(If you want to see how these spawn positions are retrieved, check out
ZombieSpawnerImplementor
. Incidentally, you may notice here that even though Components and their Implementors should ideally contain only data accessors, there are a few lines of behaviour code inside this class’s
Awake
function. These small snippets are sometimes necessary to keep implementation details encapsulated. It’s a fine balance.)
We’re not quite done, yet, however. As you’ll have gathered, a Zombie actually represents an Entity inside our ECS structure, and as such we want to make sure that we’re building a new
ZombieEntity
at the same time as its Unity GameObject is spawned.
We’ll achieve this by retrieving a list of all of our newly spawned Zombie’s Implementor classes (as these are all MonoBehaviours, they can be accessed with a simple
GetComponents
call). From there, we’ll use our
EntityFactory
member object to build a new Zombie entity, assigning it a unique Entity ID by simply retrieving its GameObject
InstanceID
.
With this new Zombie Entity built, we’ll grab its ID one more time and store it in the
lastSpawnedID
value of our
ZombieSpawnerEV
’s
spawnerComponent
. This
lastSpawnedID
variable is another DispatchOnSet property that allows us to implicitly alert other Engines that are interested in knowing about our newly spawned Zombie.
(Notice in the snippet above that we’re yielding to the next frame
before
we assign our
lastSpawnedID
value. This is deliberate, as it ensures that Svelto.ECS has completely finished building the new Zombie Entity before other Engines listening for
lastSpawnedID
start querying for it.)
Finally, we’ll want to adjust our coroutine so that it only spawns a new Zombie every few seconds (certainly not every frame). Here we’ll make use of Unity’s handy
WaitForSeconds
object. We’ll assign it as a member variable, and construct it once our
ZombieSpawnerEV
reference has been added (in the
Add
function), using the
secsBetweenSpawns
property of our
ZombieSpawnerEV
’s
spawnerComponent
(a simple
float
value currently set to 3.0).
We can then yield return our new
WaitForSeconds
object at the start of every loop iteration, to ensure a few seconds’ delay between each spawn.
Finally, let’s run our coroutine at the end of the
Add
function.
Try running the game in Unity play mode now, and you should indeed see Zombies spawning around the scene. Try to clicking on them with your mouse as well to test the Gun Engines we implemented earlier!
Besides looping a simple walking animation, however, these Zombies do not move, so once the novelty of firing at these rather boring enemies subsides, let’s by proceed by implementing their movement behaviour.
Open up the
ZombieMovementEngine
(
Assets/Scripts/Engines/Zombies/ZombieMovementEngine.cs
).
Inside its
Add
function, once our
ZombieSpawnerEV
reference has been assigned, we’ll sign up to changes to its
lastSpawnedID
. As before, let Visual Studio generate the corresponding callback function.
Inside this callback function, we’ll use our
EntityViewsDB
to retrieve a reference to the Zombie that was just spawned (using the ID passed in as the second argument to the function).
Next, we’ll access the Zombie’s
movementComponent
, and set
navMeshEnabled
to
true
to activate its Unity
NavMeshAgent
behaviour.
We want to assign the Player’s position as the
NavMeshDestination
of this Zombie so that the Zombie will move towards the Player. To do this, however, we need to retrieve a reference to the Player Entity.
Recall from the diagram earlier that the Player Entity is also defined through the
ZombieDestinationEV
for exactly this purpose. Because we happen to know that there will only ever be one such EntityView in our scene, we can just query for all
ZombieDestinationEV
s using our
EntityViewsDB
and grab the first one it returns.
With that done, we’ll simply grab the position from our
ZombieDestinationEV
’s
positionComponent
, and assign it as the
navMeshDestination
of our newly spawned Zombie.
Hit play in Unity now and you should see that the Zombies suddenly look more ominous when coming straight towards you. The illusion may be lost as you see them walk straight the camera, however. Let’s sort that out next.
Triggers and animations
We want our Zombies to stop moving once they’re within grabbing distance of the Player.
If you inspect the Unity scene, you’ll see that the
Player Camera
object has a
Sphere Collider
attached to it. The
triggerComponent
of each
ZombieEV
, meanwhile, has a
DispatchOnSet<bool>
property that will be set to true whenever a Zombie touches a Trigger Collider (check out
ZombieTriggerImplementor
to see exactly how this interfaces with the Unity collision system).
Back inside
ZombieMovementEngine
, then, let’s stop our Zombies’ movement the moment this trigger flag has been set.
We’ll start inside the callback function for a new Zombie spawn that we just wrote (here called
OnZombieSpawn
), signing up to be notified of changes to our Zombie’s
triggeredAgainstTarget
property (on its
triggerComponent
).
Inside the callback function we just assigned, we’ll simply retrieve a reference to the Zombie in question and disable its
navMeshAgent
property.
With these basic gameplay features in place, we’ll proceed to add a little more aliveness to our Zombies by triggering different animations in response to what’s happening in the game.
(If you select a Zombie GameObject in Unity and view the Animator window, you will see that its animation system has been set up to respond to two triggers, which we’ll be setting in
ZombieAnimationEngine
. You can also inspect the
ZombieAnimationImplementor
to see how the interaction between Svelto.ECS and the Unity animation happens.)
Open
ZombieAnimationEngine
(
Assets/Scripts/Engines/Zombies/ZombieAnimationEngine.cs
).
Here we’re going to do exactly the same thing we did in
ZombieMovementEngine
, by signing up a callback against our
ZombieSpawnerEV
’s
lastSpawnedID
property.
Inside this callback, we’ll sign up two additional callback functions – one for changes to the Zombie’s health (the
currentHealth
property of its
healthComponent
), and one for the Zombie triggering against the Player (the
triggeredAgainstTarget
property of its
triggerComponent
).
In the first of our new callback functions, we’ll check to see whether the Zombie’s current health (passed in as a parameter) is less than or equal to zero. If it is, we’ll retrieve a reference to the Zombie in question, and set the
trigger
property of its
animationComponent
to “deathTrigger”, activating its death animation.
Similarly, in the second callback function, we’ll set the same
trigger
value to “attackTrigger” to activate the Zombie’s attack animation once it’s touching the Player.
The final touch we’ll want to add here before we finish (as should become apparent with a little playtesting), is to disable each Zombie’s movement and trigger behaviour once their health has dropped to zero. Hop back to
ZombieMovementEngine
and make this final adjustment.
*
With that, your Zombies should now be aggressively chasing you down, yet fall over graciously when shot at enough times.
Hopefully this short tutorial has given you a sense of some of the key concepts underlying game development around ECS. In the next tutorial, we’ll expand our little game to include a GUI with a scoring system, some additional sound effects and a final game over condition.
Updated Mar 21, 2019
Version 2.0
No CommentsBe the first to comment
"}},"componentScriptGroups({\"componentId\":\"custom.widget.MicrosoftFooter\"})":{"__typename":"ComponentScriptGroups","scriptGroups":{"__typename":"ComponentScriptGroupsDefinition","afterInteractive":{"__typename":"PageScriptGroupDefinition","group":"AFTER_INTERACTIVE","scriptIds":[]},"lazyOnLoad":{"__typename":"PageScriptGroupDefinition","group":"LAZY_ON_LOAD","scriptIds":[]}},"componentScripts":[]},"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/community/NavbarDropdownToggle\"]})":[{"__ref":"CachedAsset:text:en_US-components/community/NavbarDropdownToggle-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/common/QueryHandler\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/common/QueryHandler-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageCoverImage\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageCoverImage-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeTitle\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeTitle-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageTimeToRead\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageTimeToRead-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageSubject\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageSubject-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/users/UserLink\"]})":[{"__ref":"CachedAsset:text:en_US-components/users/UserLink-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/users/UserRank\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/users/UserRank-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageTime\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageTime-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageBody\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageBody-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageCustomFields\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageCustomFields-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageRevision\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageRevision-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageReplyButton\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageReplyButton-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/messages/MessageAuthorBio\"]})":[{"__ref":"CachedAsset:text:en_US-components/messages/MessageAuthorBio-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/users/UserAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/ranks/UserRankLabel\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/users/UserRegistrationDate\"]})":[{"__ref":"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeAvatar\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeDescription\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"components/tags/TagView/TagViewChip\"]})":[{"__ref":"CachedAsset:text:en_US-components/tags/TagView/TagViewChip-1745505309750"}],"cachedText({\"lastModified\":\"1745505309750\",\"locale\":\"en-US\",\"namespaces\":[\"shared/client/components/nodes/NodeIcon\"]})":[{"__ref":"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505309750"}]},"CachedAsset:pages-1745487429335":{"__typename":"CachedAsset","id":"pages-1745487429335","value":[{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"BlogViewAllPostsPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId/all-posts/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"CasePortalPage","type":"CASE_PORTAL","urlPath":"/caseportal","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"CreateGroupHubPage","type":"GROUP_HUB","urlPath":"/groups/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"CaseViewPage","type":"CASE_DETAILS","urlPath":"/case/:caseId/:caseNumber","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"InboxPage","type":"COMMUNITY","urlPath":"/inbox","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"HelpFAQPage","type":"COMMUNITY","urlPath":"/help","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"IdeaMessagePage","type":"IDEA_POST","urlPath":"/idea/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"IdeaViewAllIdeasPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/all-ideas/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"LoginPage","type":"USER","urlPath":"/signin","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"BlogPostPage","type":"BLOG","urlPath":"/category/:categoryId/blogs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"UserBlogPermissions.Page","type":"COMMUNITY","urlPath":"/c/user-blog-permissions/page","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ThemeEditorPage","type":"COMMUNITY","urlPath":"/designer/themes","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TkbViewAllArticlesPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId/all-articles/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730142000000,"localOverride":null,"page":{"id":"AllEvents","type":"CUSTOM","urlPath":"/Events","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"OccasionEditPage","type":"EVENT","urlPath":"/event/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"OAuthAuthorizationAllowPage","type":"USER","urlPath":"/auth/authorize/allow","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"PageEditorPage","type":"COMMUNITY","urlPath":"/designer/pages","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"PostPage","type":"COMMUNITY","urlPath":"/category/:categoryId/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ForumBoardPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TkbBoardPage","type":"TKB","urlPath":"/category/:categoryId/kb/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"EventPostPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"UserBadgesPage","type":"COMMUNITY","urlPath":"/users/:login/:userId/badges","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"GroupHubMembershipAction","type":"GROUP_HUB","urlPath":"/membership/join/:nodeId/:membershipType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"MaintenancePage","type":"COMMUNITY","urlPath":"/maintenance","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"IdeaReplyPage","type":"IDEA_REPLY","urlPath":"/idea/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"UserSettingsPage","type":"USER","urlPath":"/mysettings/:userSettingsTab","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"GroupHubsPage","type":"GROUP_HUB","urlPath":"/groups","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ForumPostPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"OccasionRsvpActionPage","type":"OCCASION","urlPath":"/event/:boardId/:messageSubject/:messageId/rsvp/:responseType","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"VerifyUserEmailPage","type":"USER","urlPath":"/verifyemail/:userId/:verifyEmailToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"AllOccasionsPage","type":"OCCASION","urlPath":"/category/:categoryId/events/:boardId/all-events/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"EventBoardPage","type":"EVENT","urlPath":"/category/:categoryId/events/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TkbReplyPage","type":"TKB_REPLY","urlPath":"/kb/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"IdeaBoardPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"CommunityGuideLinesPage","type":"COMMUNITY","urlPath":"/communityguidelines","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"CaseCreatePage","type":"SALESFORCE_CASE_CREATION","urlPath":"/caseportal/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TkbEditPage","type":"TKB","urlPath":"/kb/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ForgotPasswordPage","type":"USER","urlPath":"/forgotpassword","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"IdeaEditPage","type":"IDEA","urlPath":"/idea/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TagPage","type":"COMMUNITY","urlPath":"/tag/:tagName","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"BlogBoardPage","type":"BLOG","urlPath":"/category/:categoryId/blog/:boardId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"OccasionMessagePage","type":"OCCASION_TOPIC","urlPath":"/event/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ManageContentPage","type":"COMMUNITY","urlPath":"/managecontent","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ClosedMembershipNodeNonMembersPage","type":"GROUP_HUB","urlPath":"/closedgroup/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"CommunityPage","type":"COMMUNITY","urlPath":"/","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ForumMessagePage","type":"FORUM_TOPIC","urlPath":"/discussions/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"IdeaPostPage","type":"IDEA","urlPath":"/category/:categoryId/ideas/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730142000000,"localOverride":null,"page":{"id":"CommunityHub.Page","type":"CUSTOM","urlPath":"/Directory","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"BlogMessagePage","type":"BLOG_ARTICLE","urlPath":"/blog/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"RegistrationPage","type":"USER","urlPath":"/register","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"EditGroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ForumEditPage","type":"FORUM","urlPath":"/discussions/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ResetPasswordPage","type":"USER","urlPath":"/resetpassword/:userId/:resetPasswordToken","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1730142000000,"localOverride":null,"page":{"id":"AllBlogs.Page","type":"CUSTOM","urlPath":"/blogs","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TkbMessagePage","type":"TKB_ARTICLE","urlPath":"/kb/:boardId/:messageSubject/:messageId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"BlogEditPage","type":"BLOG","urlPath":"/blog/:boardId/:messageSubject/:messageId/edit","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ManageUsersPage","type":"USER","urlPath":"/users/manage/:tab?/:manageUsersTab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ForumReplyPage","type":"FORUM_REPLY","urlPath":"/discussions/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"PrivacyPolicyPage","type":"COMMUNITY","urlPath":"/privacypolicy","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"NotificationPage","type":"COMMUNITY","urlPath":"/notifications","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"UserPage","type":"USER","urlPath":"/users/:login/:userId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"OccasionReplyPage","type":"OCCASION_REPLY","urlPath":"/event/:boardId/:messageSubject/:messageId/comments/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ManageMembersPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/manage/:tab?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"SearchResultsPage","type":"COMMUNITY","urlPath":"/search","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"BlogReplyPage","type":"BLOG_REPLY","urlPath":"/blog/:boardId/:messageSubject/:messageId/replies/:replyId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"GroupHubPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TermsOfServicePage","type":"COMMUNITY","urlPath":"/termsofservice","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"CategoryPage","type":"CATEGORY","urlPath":"/category/:categoryId","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"ForumViewAllTopicsPage","type":"FORUM","urlPath":"/category/:categoryId/discussions/:boardId/all-topics/(/:after|/:before)?","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"TkbPostPage","type":"TKB","urlPath":"/category/:categoryId/kbs/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"},{"lastUpdatedTime":1745487429335,"localOverride":null,"page":{"id":"GroupHubPostPage","type":"GROUP_HUB","urlPath":"/group/:groupHubId/:boardId/create","__typename":"PageDescriptor"},"__typename":"PageResource"}],"localOverride":false},"CachedAsset:text:en_US-components/context/AppContext/AppContextProvider-0":{"__typename":"CachedAsset","id":"text:en_US-components/context/AppContext/AppContextProvider-0","value":{"noCommunity":"Cannot find community","noUser":"Cannot find current user","noNode":"Cannot find node with id {nodeId}","noMessage":"Cannot find message with id {messageId}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/Loading/LoadingDot-0":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Loading/LoadingDot-0","value":{"title":"Loading..."},"localOverride":false},"User:user:-1":{"__typename":"User","id":"user:-1","uid":-1,"login":"Deleted","email":"","avatar":null,"rank":null,"kudosWeight":1,"registrationData":{"__typename":"RegistrationData","status":"ANONYMOUS","registrationTime":null,"confirmEmailStatus":false,"registrationAccessLevel":"VIEW","ssoRegistrationFields":[]},"ssoId":null,"profileSettings":{"__typename":"ProfileSettings","dateDisplayStyle":{"__typename":"InheritableStringSettingWithPossibleValues","key":"layout.friendly_dates_enabled","value":"false","localValue":"true","possibleValues":["true","false"]},"dateDisplayFormat":{"__typename":"InheritableStringSetting","key":"layout.format_pattern_date","value":"MMM dd yyyy","localValue":"MM-dd-yyyy"},"language":{"__typename":"InheritableStringSettingWithPossibleValues","key":"profile.language","value":"en-US","localValue":"en","possibleValues":["en-US"]}},"deleted":false},"Theme:customTheme1":{"__typename":"Theme","id":"customTheme1"},"Category:category:EducationSector":{"__typename":"Category","id":"category:EducationSector","entityType":"CATEGORY","displayId":"EducationSector","nodeType":"category","depth":3,"title":"Education Sector","shortTitle":"Education Sector","parent":{"__ref":"Category:category:solutions"},"categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:top":{"__typename":"Category","id":"category:top","displayId":"top","nodeType":"category","depth":0,"title":"Top","entityType":"CATEGORY","shortTitle":"Top"},"Category:category:communities":{"__typename":"Category","id":"category:communities","displayId":"communities","nodeType":"category","depth":1,"parent":{"__ref":"Category:category:top"},"title":"Communities","entityType":"CATEGORY","shortTitle":"Communities"},"Category:category:solutions":{"__typename":"Category","id":"category:solutions","displayId":"solutions","nodeType":"category","depth":2,"parent":{"__ref":"Category:category:communities"},"title":"Topics","entityType":"CATEGORY","shortTitle":"Topics"},"Blog:board:EducatorDeveloperBlog":{"__typename":"Blog","id":"board:EducatorDeveloperBlog","entityType":"BLOG","displayId":"EducatorDeveloperBlog","nodeType":"board","depth":4,"conversationStyle":"BLOG","title":"Educator Developer Blog","description":"","avatar":null,"profileSettings":{"__typename":"ProfileSettings","language":null},"parent":{"__ref":"Category:category:EducationSector"},"ancestors":{"__typename":"CoreNodeConnection","edges":[{"__typename":"CoreNodeEdge","node":{"__ref":"Community:community:gxcuf89792"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:communities"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:solutions"}},{"__typename":"CoreNodeEdge","node":{"__ref":"Category:category:EducationSector"}}]},"userContext":{"__typename":"NodeUserContext","canAddAttachments":false,"canUpdateNode":false,"canPostMessages":false,"isSubscribed":false},"boardPolicies":{"__typename":"BoardPolicies","canPublishArticleOnCreate":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.forums.policy_can_publish_on_create_workflow_action.accessDenied","key":"error.lithium.policies.forums.policy_can_publish_on_create_workflow_action.accessDenied","args":[]}}},"shortTitle":"Educator Developer Blog","repliesProperties":{"__typename":"RepliesProperties","sortOrder":"REVERSE_PUBLISH_TIME","repliesFormat":"threaded"},"tagProperties":{"__typename":"TagNodeProperties","tagsEnabled":{"__typename":"PolicyResult","failureReason":null}},"requireTags":false,"tagType":"FREEFORM_ONLY"},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc","height":512,"width":512,"mimeType":"image/png"},"Rank:rank:4":{"__typename":"Rank","id":"rank:4","position":6,"name":"Microsoft","color":"333333","icon":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/cmstNC05WEo0blc\"}"},"rankStyle":"OUTLINE"},"User:user:210546":{"__typename":"User","id":"user:210546","uid":210546,"login":"Lee_Stott","deleted":false,"avatar":{"__typename":"UserAvatar","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/dS0yMTA1NDYtODM5MjVpMDI2ODNGQTMwMzAwNDFGQQ"},"rank":{"__ref":"Rank:rank:4"},"email":"","messagesCount":380,"biography":null,"topicsCount":349,"kudosReceivedCount":432,"kudosGivenCount":13,"kudosWeight":1,"registrationData":{"__typename":"RegistrationData","status":null,"registrationTime":"2018-09-25T06:34:59.520-07:00","confirmEmailStatus":null},"followersCount":null,"solutionsCount":0},"BlogTopicMessage:message:381053":{"__typename":"BlogTopicMessage","uid":381053,"subject":"Entity-Component System in Unity – a tutorial","id":"message:381053","revisionNum":2,"repliesCount":0,"author":{"__ref":"User:user:210546"},"depth":0,"hasGivenKudo":false,"board":{"__ref":"Blog:board:EducatorDeveloperBlog"},"conversation":{"__ref":"Conversation:conversation:381053"},"messagePolicies":{"__typename":"MessagePolicies","canPublishArticleOnEdit":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.forums.policy_can_publish_on_edit_workflow_action.accessDenied","key":"error.lithium.policies.forums.policy_can_publish_on_edit_workflow_action.accessDenied","args":[]}},"canModerateSpamMessage":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.feature.moderation_spam.action.moderate_entity.allowed.accessDenied","key":"error.lithium.policies.feature.moderation_spam.action.moderate_entity.allowed.accessDenied","args":[]}}},"contentWorkflow":{"__typename":"ContentWorkflow","state":"PUBLISH","scheduledPublishTime":null,"scheduledTimezone":null,"userContext":{"__typename":"MessageWorkflowContext","canSubmitForReview":null,"canEdit":false,"canRecall":null,"canSubmitForPublication":null,"canReturnToAuthor":null,"canPublish":null,"canReturnToReview":null,"canSchedule":false},"shortScheduledTimezone":null},"readOnly":false,"editFrozen":false,"moderationData":{"__ref":"ModerationData:moderation_data:381053"},"teaser":"First published on MSDN on May 08, 2018 Guest post by Sondre Agledahl: Microsoft Student Partner, Games programmer and CS student at UCL.","body":"\n \n \n First published on MSDN on May 08, 2018\n \n \n
\n \n
\n
\n \n
\n
\n \n
\n
\n Guest post by Sondre Agledahl: Microsoft Student Partner, Games programmer and CS student at UCL.\n
\n
\n Object-orientated programming is dead! Long live ECS!\n
\n
\n Entity-Component System is a powerful architectural pattern often used in game development. It’s in keeping with the general software architecture trend away from inheritance and object-orientated principles to a more data-driven approach to programming. ECS aims to make codebases as modular and memory-efficient as possible through a number of unique design principles, key of which is a clear separation between data and behaviour.\n
\n
\n \n Unity’s recent talks\n \n have made it clear that they are embracing ECS and slowly rolling it out in favour of its old object-component model, so it is high time to familiarise oneself with this approach to writing code.\n
\n
\n Before Unity’s official new structure is deployed, however, you will have ample opportunity to try out this system through numerous open-source ECS frameworks compatible with Unity. This 2-part tutorial explores the Svelto.ECS framework\n \n (actively maintained on GitHub\n \n by creator Sebastiano Mandalà), and will help you build a complete zombie survival shooter game (pictured below) using Svelto.ECS. Through the concepts you’ll explore by implementing this game, you should gather the experience to make your own ECS-based Unity games from scratch.\n
\n
\n \n
\n
\n (This tutorial assumes familiarity with Unity’s standard MonoBehaviour structure. If you’ve already built a couple of simple games in Unity and have passing knowledge about its key features, you should be good to go.)\n
\n
\n ECS – A primer\n
\n
\n The core idea of ECS is contained in\n \n Entities\n \n , which are actually not altogether different from the basic\n \n GameObject\n \n s in Unity, as they are objects that define tangible\n \n things\n \n inside your game. These Entities are simply containers for the\n \n Components\n \n that are attached to them (such as\n \n positionComponent, healthComponent, movementComponent\n \n , etc.). Where this structure differs from Unity’s standard models (and OOP design generally), is that neither Entities nor Components have any behaviour defined on them – they contain no class methods, only dumb data as member variables.\n
\n
\n Rather, all the behaviour that acts on Entities and their Components is contained inside\n \n Engines\n \n (also known in some ECS frameworks as\n \n Systems\n \n ). Inside Engines, all of the methods that define how certain Entities should behave are executed. In this way, rather than having each object with a movement script executing its own movement function to move, in ECS a single MovementEngine acts on all Entities with MovementComponents to create the same result.\n
\n
\n The final key concept found in Svelto.ECS is the idea of\n \n EntityViews\n \n . An EntityView is simply a wrapper around each Entity that Engines use to interact with different Entities in a polymorphic way. Several of these EntityView wrappers can be defined for each Entity depending on the different roles the Entity might play in each engine.\n
\n
\n For a tangible example, in our game, the Player Entity plays several unrelated roles: Firstly, it needs to store data about player input created by the\n \n PlayerInput\n \n Engine to indicate where the player is aiming their gun. It also, however, needs to store a position for the\n \n ZombieMovement\n \n Engine to read to cause all Zombies to move towards the player’s position. These two roles do not (and really should not) need to interact whatsoever, which is why the Player Entity has two EntityViews (EV) defined:\n \n PlayerEV\n \n and\n \n ZombieDestinationEV\n \n . These two EntityViews, despite referring to the same Entity under the hood, each contain references only to those Components that are relevant to their one purpose.\n
\n
\n The diagram below shows the structure of our game that we will cover in this tutorial.\n
\n
\n \n
\n
\n If all of that sounds very abstract, then let’s get started making a game and make things a little more tangible. We’ll explain concepts in more detail as they become relevant.\n
\n
\n Shooting guns and reading input\n
\n
\n Download\n \n the project skeleton from GitHub\n \n and open it in Unity. This project includes a copy of Svelto.ECS (and its utility libraries\n \n Svelto.Tasks\n \n and\n \n Svelto.Common\n \n ), as well as a half-finished game structure.\n
\n
\n All Entities, Components and Engines necessary for the game have been defined already (according to the diagram above), but it’s up to us to write the Engines’ behaviour so they do what we want them to.\n
\n
\n (Have a look around the project to get a feel for it if you’d like. Notice that Entities themselves are defined only in tiny little class definitions (\n \n EntityDescriptors\n \n ), whereas their respective EntityViews are what define which Components each of them actually hold. You may also notice the mysterious\n \n Implementor\n \n classes, which we will cover later – though the astute reader can probably gather what purpose they serve with a little nosing around. If you’re curious about how all Entities and Engines are initialised, have a peek in the\n \n MainContext.cs\n \n file (which is attached to the\n \n GameContext\n \n GameObject in the Unity scene).)\n
\n
\n Let’s begin by taking a look at the\n \n GunShootingEngine\n \n (\n \n Assets/Scripts/Engines/Player/GunShootingEngine.cs\n \n ) in Visual Studio (or any code editor of your choice). As the name implies, this class will control the shooting behaviour of the Player’s gun.\n \n
\n
\n You’ll notice that\n \n GunShootingEngine\n \n is a\n \n “SingleEntityViewEngine”\n \n for the\n \n PlayerEV\n \n , which simply means it will receive a reference to\n \n PlayerEV\n \n when the game begins through its\n \n Add\n \n method (where you can see we’re storing the reference as a member variable for safekeeping).\n
\n
\n \n GunShootingEngine\n \n is not, however, a\n \n MonoBehaviour\n \n , which means it will not have any\n \n Awake, Start\n \n or\n \n Update\n \n functions called automatically by Unity as we might be used to. How, then, are we going to ensure this Engine is updated every frame? With a\n \n coroutine\n \n .\n
\n
\n We’ll start by defining our coroutine in the usual way as a method that returns an IEnumerator. Every time this method updates, it will check to see whether the Player wants to fire their gun, so name it accordingly.\n
\n
\n \n
\n
\n Inside our new coroutine, we will very simply loop forever, checking at every iteration whether the value of\n \n isFiring\n \n inside our\n \n PlayerEV\n \n ’s\n \n inputComponent\n \n is set to\n \n true\n \n . Being wary of infinite loops, we remember to\n \n yield return null\n \n at the end of our loop to make sure it only continues its next iteration in the next frame.\n
\n \n If\n \n isFiring\n \n is indeed true, we will call a wishfully defined\n \n Shoot\n \n function – you can go ahead and define that now (as a simple void no-argument function), or simply let Visual Studio auto-generate it for you (move your cursor to your function call and press ALT+ENTER).\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n Before we implement our new function, let’s satisfy one likely point of curiosity: Where is the value of\n \n isFiring\n \n being set? A very good question we should address right away. Let’s hop over to\n \n PlayerInputEngine\n \n (\n \n Assets/Scripts/Engines/Player/PlayerInputEngine.cs\n \n ) and make sure it’s doing what we want it to do before we proceed.\n
\n
\n \n
\n
\n \n PlayerInputEngine\n \n is a\n \n SingleEntityViewEngine\n \n for the\n \n PlayerEV\n \n as well. The purpose of this engine will be to continuously read input from the Player’s mouse and store these as values in the\n \n PlayerEV\n \n ’s\n \n inputComponent\n \n .\n
\n
\n (You can have a look at the definition of the Player’s\n \n inputComponent\n \n (\n \n Assets/Scripts/Components/Player/PlayerInputComponent.cs\n \n ) to get a sense of the values this engine will modify.)\n
\n
\n Let’s make a coroutine to do this input reading; as per usual, we’ll want an infinite loop inside it that yields at the end of each iteration.\n
\n
\n \n
\n
\n There are three\n \n inputComponent\n \n values that we want to update here:\n \n aimPos\n \n (which is the screen space position of the mouse),\n \n isFiring\n \n (which is a boolean indicating whether the Player clicked their primary mouse button this frame) and\n \n aimRay\n \n (which will be a Unity Ray that points straight forward from the Player’s mouse position into world space).\n
\n
\n For the first two, we’ll simply take Unity’s\n \n Input.Position\n \n and\n \n Input.GetButtonDown(“Fire1”)\n \n values and store them as-is. Then we’ll use a function of our member variable\n \n m_Camera\n \n (which is simply a wrapper around Unity’s camera utility functions),\n \n \n ScreenPointToRay\n \n \n , to convert the Player’s mouse position into a Ray for us to store.\n
\n
\n \n
\n
\n Finally, we’ll call our coroutine as soon as the Engine boots up inside the\n \n Add\n \n function. Again, since our Engines are not\n \n MonoBehaviour\n \n s, we don’t have access to Unity’s\n \n StartCoroutine\n \n function, but the Svelto.ECS utility library Svelto.Tasks lets us do exactly the same thing (in fact, even more efficiently!) by simply typing\n \n ReadInput().Run()\n \n .\n
\n
\n \n
\n
\n With Player Input neatly sorted, let’s return to\n \n GunShootingEngine\n \n to complete it. now that we know we’re reading accurate input values.\n
\n
\n Inside our\n \n Shoot\n \n function in\n \n GunShootingEngine\n \n , we want to do three things: Check whether the Ray from the Player’s mouse is touching a Collider object, check whether that object is a Zombie, and – if it is, decrement its health. We’ll also want to store a reference to the point of impact in a\n \n Vector3\n \n for reasons that will become clear presently. Feel free to copy the code below, or implement it gradually as we go through it.\n
\n
\n \n
\n
\n The\n \n m_RayCaster\n \n member object is just another wrapper around Unity’s RayCasting functionality, and its function\n \n GetRayHitTarget\n \n will return the Entity ID of the first Entity that was hit by the Player’s Ray, or -1 if nothing was hit.\n
\n
\n If we did in fact hit something, we make use of our\n \n IEntityViewsDatabase\n \n property, which (as the name implies) is a database of all the EntityViews currently alive in the scene.\n
\n
\n (Where did this property magically appear from? Notice that\n \n GunShootingEngine\n \n implements the eloquently named\n \n IQueryingEntityViewEngine\n \n interface. All engines that implement this interface will automatically have a reference to this database assigned to them when Svelto.ECS initialises. It will quickly become apparent that these databases are crucial for engines to function properly.)\n
\n
\n With this EntityView database, we check to see whether the Entity we hit is a\n \n GunTargetEV\n \n using the\n \n TryQueryEntityView\n \n function.\n
\n
\n (Recall from the diagram earlier that\n \n GunTargetEV\n \n is really just a synonym for the Zombie Entity. (Or, more accurately,\n \n GunTargetEV\n \n is one of the EntityViews that define what a Zombie is.))\n
\n
\n If it is, we’ll access that GunTarget’s\n \n healthComponent\n \n , and decrease its\n \n currentHealth\n \n value by the\n \n damagePerShot\n \n value defined in our\n \n PlayerEV’s\n \n \n gunComponent\n \n (this is just a simple\n \n int\n \n value currently set to 1 that we can customise later). Finally, we’ll store the position of our\n \n impactPoint\n \n (the point where our bullet touched the Zombie) in the\n \n lastImpactPos\n \n value of our\n \n PlayerEV’s gunComponent\n \n .\n
\n
\n Remembering to start our\n \n CheckForFire\n \n coroutine right after our\n \n PlayerEV\n \n reference has been assigned, we now have functioning gun shooting behaviour in our game. With a little bit of sound and visual effects we’ll be able to see the impact of this Engine.\n
\n
\n \n
\n
\n Now, if you diligently copied the lines of code into\n \n Shoot\n \n from above, you might still be curious why the two variables we assigned to at the end of\n \n Shoot\n \n have a\n \n .value\n \n property attached at the end, and what this could be useful for. Rightfully so – let’s explore that right away.\n
\n
\n Broadcasting messages and splattering blood\n
\n
\n If you take a look at the definition of\n \n PlayerEV’s gunComponent\n \n (\n \n Assets/Scripts/Components/Player/GunComponent.cs\n \n ), you’ll notice that\n \n lastImpactPos\n \n is not just a regular\n \n Vector3\n \n , but actually a\n \n “DispatchOnSet<Vector3>”\n \n .\n
\n
\n This\n \n DispatchOnSet\n \n data type actually represents one of the primary means of engines communicating with each other in Svelto.ECS.\n
\n
\n (There are actually several useful methods for communicating across classes in Svelto that are equally modular (and I would encourage you to try them out!), but for the sake of simplicity we will only use\n \n DispatchOnSet\n \n in this tutorial).\n
\n
\n \n DispatchOnSet\n \n is a wrapper around a simple value-type variable, but is also a self-contained Observer-Listener object. Any class that can access a\n \n DispatchOnSet\n \n property can sign up callback functions to be notified whenever the value it refers to is changed.\n
\n
\n So, in the case of our\n \n lastImpactPos\n \n variable, the moment we changed its value inside\n \n GunShootingEngine\n \n , every class that signed up to listen for that change would be immediately notified. At the moment, however,\n \n lastImpactPos\n \n has no subscribers, so let’s do something about that.\n
\n
\n Navigate to the\n \n GunEffectsEngine\n \n (\n \n Assets/Engines/Player/GunEffectsEngine.cs\n \n ). This engine, too, works on the single EntityView\n \n PlayerEV\n \n . Inside here we’re going to instantiate a simple blood splattering particle effect whenever the player’s gun successfully shoots a zombie.\n
\n
\n \n
\n
\n Inside the Add function, right after we’ve been assigned a reference to\n \n PlayerEV\n \n , we’ll sign up to be notified of changes to\n \n lastImpactPos\n \n . To do this, simply access\n \n PlayerEV\n \n ’s\n \n gunComponent\n \n , and call\n \n lastImpactPos’s\n \n \n NotifyOnValueSet\n \n function, passing in a wishfully titled callback function name.\n
\n
\n \n
\n
\n Again, you can let Visual Studio auto-generate an appropriate function for this purpose (simply select the function name you passed in and hit ALT+ENTER).\n
\n
\n \n
\n
\n As you’ll note from Visual Studio’s auto-generated parameters (which we can rename once we know what they refer to), this function will take in an\n \n int\n \n , which is the ID of the Entity that holds the value that was just changed, and the changed value itself (in this case, a\n \n Vector3\n \n ).\n
\n
\n If you recall from your look at the\n \n gunComponent\n \n earlier, it already contains a\n \n gunEffectPrefab\n \n variable (where this has been assigned we will cover in just a moment). Let’s retrieve that and instantiate it at the gun’s impact position.\n
\n
\n \n
\n
\n We instantiate our prefab using the\n \n Build\n \n function of our member object\n \n m_GameObjectFactory\n \n .\n
\n
\n (\n \n GameObjectFactory\n \n is (for our purposes, anyway) little more than a wrapper around Unity’s familiar\n \n Instantiate\n \n function that is passed in manually through\n \n GunEffectsEngine\n \n ’s constructor\n \n \n – check out\n \n MainContext.cs\n \n to see where the construction of this object happens.)\n
\n
\n With the prefab instantiated, we assign its position to the new bullet impact position that we just had passed in.\n
\n
\n With that, we now have bullets in the game that splatter blood particles on impact (you can check and tweak the blood effect by inspecting the prefab (\n \n Assets/Prefabs/Blood.prefab\n \n )). Of course, until we have Zombies spawning, we have little to test this functionality against.\n
\n
\n Before we proceed with developing our Zombie-spawning functionality, however, let’s take a moment to unwrap the final key piece of Svelto.ECS that we’ve only skimmed over up to this point:\n \n Implementors\n \n .\n
\n
\n So far, we’ve accessed Components belonging to several different Entities, using them both to read constant data (such as\n \n damagePerShot\n \n and\n \n bloodEffectPrefab\n \n ), and to edit data for other Engines to read (such as\n \n currentHealth\n \n and\n \n lastImpactPos\n \n ).\n
\n
\n If you’ve inspected the definitions of these Components, you’ll have seen that they are simply interfaces that declare the existence of these properties. This is another deliberate design decision to keep Svelto.ECS codebases as modular as possible, but it raises the immediate practical question – where are they implemented? Investigate the\n \n Assets/Scripts/Implementors\n \n folder (and its subfolders) to get a clear answer.\n
\n
\n \n
\n
\n These\n \n Implementor\n \n classes provide specific class implementations of every Component in the game. While some of them (notably\n \n PlayerInputImplementor\n \n ) are very straightforward classes that simply define the properties laid out in their interface, others (\n \n GunImplementor\n \n , and several of the Zombie Implementors which we’ll address in a moment) are subclasses of\n \n MonoBehaviour\n \n as well, and contain some interaction with Unity.\n
\n
\n Implementors are where these traditional Unity interactions can take place (such as receiving a\n \n OnTriggerEnter\n \n callback or receiving a\n \n SerializeField\n \n property from the Unity Inspector).\n
\n
\n (Notice, for example, that the\n \n bloodEffectPrefab\n \n we accessed in\n \n GunEffectsEngine\n \n is simply a serialised GameObject variable that has been dragged in from the\n \n Assets/Prefabs\n \n folder onto the\n \n GunImplementor\n \n MonoBehaviour script attached to the\n \n Player Camera\n \n object in our Unity scene.)\n
\n
\n Implementors, in other words, are the bridge between Unity and Svelto.ECS, allowing the rest of the codebase to be (mostly) independent from how Unity works.\n
\n
\n With that little aside, let’s return to our Engine development, with a mind towards getting Zombies spawning in the game.\n
\n
\n Spawning zombies and making them move\n
\n
\n If you’ve looked around our Unity scene, you’ll have seen there’s a\n \n ZombieSpawner\n \n object with a number of empty child transforms called\n \n SpawnPosition\n \n . These are the places we’ll want our Zombies to spawn from.\n
\n
\n Open the\n \n ZombieSpawnerEngine\n \n (\n \n Assets/Scripts/Engines/Zombies/ZombieSpawnerEngine.cs\n \n ), and you’ll the same basic structure as the other engines we’ve looked at.\n
\n
\n (You can inspect the definition of the ZombieSpawnerEV, and its\n \n zombieSpawnerComponent\n \n to get an idea of the data we’ll be retrieving and manipulating here).\n
\n
\n The purpose of this engine is very simply to spawn a new zombie entity every few seconds. Let’s create another coroutine to do this.\n
\n
\n \n
\n
\n Inside this coroutine we’re going to loop infinitely, at every iteration picking a random spawn position from the spawnPosition field of our\n \n ZombieSpawnerEV\n \n ’s spawnerComponent. Finally we’ll build that same component’s\n \n zombieToSpawn\n \n prefab similar to how we’ve done before, and place it at the randomly chosen spawn position.\n
\n
\n \n
\n
\n (If you want to see how these spawn positions are retrieved, check out\n \n ZombieSpawnerImplementor\n \n . Incidentally, you may notice here that even though Components and their Implementors should ideally contain only data accessors, there are a few lines of behaviour code inside this class’s\n \n Awake\n \n function. These small snippets are sometimes necessary to keep implementation details encapsulated. It’s a fine balance.)\n
\n
\n We’re not quite done, yet, however. As you’ll have gathered, a Zombie actually represents an Entity inside our ECS structure, and as such we want to make sure that we’re building a new\n \n ZombieEntity\n \n at the same time as its Unity GameObject is spawned.\n
\n
\n We’ll achieve this by retrieving a list of all of our newly spawned Zombie’s Implementor classes (as these are all MonoBehaviours, they can be accessed with a simple\n \n GetComponents\n \n call). From there, we’ll use our\n \n EntityFactory\n \n member object to build a new Zombie entity, assigning it a unique Entity ID by simply retrieving its GameObject\n \n InstanceID\n \n .\n
\n
\n \n
\n
\n With this new Zombie Entity built, we’ll grab its ID one more time and store it in the\n \n lastSpawnedID\n \n value of our\n \n ZombieSpawnerEV\n \n ’s\n \n spawnerComponent\n \n . This\n \n lastSpawnedID\n \n variable is another DispatchOnSet property that allows us to implicitly alert other Engines that are interested in knowing about our newly spawned Zombie.\n
\n
\n \n
\n
\n (Notice in the snippet above that we’re yielding to the next frame\n \n before\n \n we assign our\n \n lastSpawnedID\n \n value. This is deliberate, as it ensures that Svelto.ECS has completely finished building the new Zombie Entity before other Engines listening for\n \n lastSpawnedID\n \n start querying for it.)\n
\n
\n Finally, we’ll want to adjust our coroutine so that it only spawns a new Zombie every few seconds (certainly not every frame). Here we’ll make use of Unity’s handy\n \n \n WaitForSeconds\n \n \n \n \n object. We’ll assign it as a member variable, and construct it once our\n \n ZombieSpawnerEV\n \n reference has been added (in the\n \n Add\n \n function), using the\n \n secsBetweenSpawns\n \n property of our\n \n ZombieSpawnerEV\n \n ’s\n \n spawnerComponent\n \n (a simple\n \n float\n \n value currently set to 3.0).\n
\n
\n \n
\n
\n \n
\n
\n We can then yield return our new\n \n WaitForSeconds\n \n object at the start of every loop iteration, to ensure a few seconds’ delay between each spawn.\n
\n
\n \n
\n
\n Finally, let’s run our coroutine at the end of the\n \n Add\n \n function.\n
\n
\n \n
\n
\n Try running the game in Unity play mode now, and you should indeed see Zombies spawning around the scene. Try to clicking on them with your mouse as well to test the Gun Engines we implemented earlier!\n
\n
\n Besides looping a simple walking animation, however, these Zombies do not move, so once the novelty of firing at these rather boring enemies subsides, let’s by proceed by implementing their movement behaviour.\n
\n
\n Open up the\n \n ZombieMovementEngine\n \n (\n \n Assets/Scripts/Engines/Zombies/ZombieMovementEngine.cs\n \n ).\n
\n
\n Inside its\n \n Add\n \n function, once our\n \n ZombieSpawnerEV\n \n reference has been assigned, we’ll sign up to changes to its\n \n lastSpawnedID\n \n . As before, let Visual Studio generate the corresponding callback function.\n
\n
\n \n
\n
\n \n
\n
\n Inside this callback function, we’ll use our\n \n EntityViewsDB\n \n to retrieve a reference to the Zombie that was just spawned (using the ID passed in as the second argument to the function).\n
\n
\n \n
\n
\n Next, we’ll access the Zombie’s\n \n movementComponent\n \n , and set\n \n navMeshEnabled\n \n to\n \n true\n \n to activate its Unity\n \n NavMeshAgent\n \n behaviour.\n
\n
\n \n
\n
\n We want to assign the Player’s position as the\n \n NavMeshDestination\n \n of this Zombie so that the Zombie will move towards the Player. To do this, however, we need to retrieve a reference to the Player Entity.\n
\n
\n Recall from the diagram earlier that the Player Entity is also defined through the\n \n ZombieDestinationEV\n \n for exactly this purpose. Because we happen to know that there will only ever be one such EntityView in our scene, we can just query for all\n \n ZombieDestinationEV\n \n s using our\n \n EntityViewsDB\n \n and grab the first one it returns.\n
\n
\n \n
\n
\n With that done, we’ll simply grab the position from our\n \n ZombieDestinationEV\n \n ’s\n \n positionComponent\n \n , and assign it as the\n \n navMeshDestination\n \n of our newly spawned Zombie.\n
\n
\n Hit play in Unity now and you should see that the Zombies suddenly look more ominous when coming straight towards you. The illusion may be lost as you see them walk straight the camera, however. Let’s sort that out next.\n
\n
\n Triggers and animations\n
\n
\n We want our Zombies to stop moving once they’re within grabbing distance of the Player.\n
\n
\n If you inspect the Unity scene, you’ll see that the\n \n Player Camera\n \n object has a\n \n Sphere Collider\n \n attached to it. The\n \n triggerComponent\n \n of each\n \n ZombieEV\n \n , meanwhile, has a\n \n DispatchOnSet<bool>\n \n property that will be set to true whenever a Zombie touches a Trigger Collider (check out\n \n ZombieTriggerImplementor\n \n to see exactly how this interfaces with the Unity collision system).\n
\n
\n Back inside\n \n ZombieMovementEngine\n \n , then, let’s stop our Zombies’ movement the moment this trigger flag has been set.\n
\n
\n We’ll start inside the callback function for a new Zombie spawn that we just wrote (here called\n \n OnZombieSpawn\n \n ), signing up to be notified of changes to our Zombie’s\n \n triggeredAgainstTarget\n \n property (on its\n \n triggerComponent\n \n ).\n
\n
\n \n
\n
\n Inside the callback function we just assigned, we’ll simply retrieve a reference to the Zombie in question and disable its\n \n navMeshAgent\n \n property.\n
\n
\n \n
\n
\n With these basic gameplay features in place, we’ll proceed to add a little more aliveness to our Zombies by triggering different animations in response to what’s happening in the game.\n
\n
\n (If you select a Zombie GameObject in Unity and view the Animator window, you will see that its animation system has been set up to respond to two triggers, which we’ll be setting in\n \n ZombieAnimationEngine\n \n . You can also inspect the\n \n ZombieAnimationImplementor\n \n to see how the interaction between Svelto.ECS and the Unity animation happens.)\n
\n Here we’re going to do exactly the same thing we did in\n \n ZombieMovementEngine\n \n , by signing up a callback against our\n \n ZombieSpawnerEV\n \n ’s\n \n lastSpawnedID\n \n property.\n
\n
\n \n
\n
\n Inside this callback, we’ll sign up two additional callback functions – one for changes to the Zombie’s health (the\n \n currentHealth\n \n property of its\n \n healthComponent\n \n ), and one for the Zombie triggering against the Player (the\n \n triggeredAgainstTarget\n \n property of its\n \n triggerComponent\n \n ).\n
\n
\n \n
\n
\n In the first of our new callback functions, we’ll check to see whether the Zombie’s current health (passed in as a parameter) is less than or equal to zero. If it is, we’ll retrieve a reference to the Zombie in question, and set the\n \n trigger\n \n property of its\n \n animationComponent\n \n to “deathTrigger”, activating its death animation.\n
\n
\n \n
\n
\n Similarly, in the second callback function, we’ll set the same\n \n trigger\n \n value to “attackTrigger” to activate the Zombie’s attack animation once it’s touching the Player.\n
\n
\n \n
\n
\n The final touch we’ll want to add here before we finish (as should become apparent with a little playtesting), is to disable each Zombie’s movement and trigger behaviour once their health has dropped to zero. Hop back to\n \n ZombieMovementEngine\n \n and make this final adjustment.\n
\n
\n \n
\n
\n \n
\n
\n *\n
\n
\n With that, your Zombies should now be aggressively chasing you down, yet fall over graciously when shot at enough times.\n
\n
\n Hopefully this short tutorial has given you a sense of some of the key concepts underlying game development around ECS. In the next tutorial, we’ll expand our little game to include a GUI with a scoring system, some additional sound effects and a final game over condition.\n
\n \n","body@stringLength":"37653","rawBody":"\n \n \n First published on MSDN on May 08, 2018\n \n \n
\n \n
\n
\n \n
\n
\n \n
\n
\n Guest post by Sondre Agledahl: Microsoft Student Partner, Games programmer and CS student at UCL.\n
\n
\n Object-orientated programming is dead! Long live ECS!\n
\n
\n Entity-Component System is a powerful architectural pattern often used in game development. It’s in keeping with the general software architecture trend away from inheritance and object-orientated principles to a more data-driven approach to programming. ECS aims to make codebases as modular and memory-efficient as possible through a number of unique design principles, key of which is a clear separation between data and behaviour.\n
\n
\n \n Unity’s recent talks\n \n have made it clear that they are embracing ECS and slowly rolling it out in favour of its old object-component model, so it is high time to familiarise oneself with this approach to writing code.\n
\n
\n Before Unity’s official new structure is deployed, however, you will have ample opportunity to try out this system through numerous open-source ECS frameworks compatible with Unity. This 2-part tutorial explores the Svelto.ECS framework\n \n (actively maintained on GitHub\n \n by creator Sebastiano Mandalà), and will help you build a complete zombie survival shooter game (pictured below) using Svelto.ECS. Through the concepts you’ll explore by implementing this game, you should gather the experience to make your own ECS-based Unity games from scratch.\n
\n
\n \n
\n
\n (This tutorial assumes familiarity with Unity’s standard MonoBehaviour structure. If you’ve already built a couple of simple games in Unity and have passing knowledge about its key features, you should be good to go.)\n
\n
\n ECS – A primer\n
\n
\n The core idea of ECS is contained in\n \n Entities\n \n , which are actually not altogether different from the basic\n \n GameObject\n \n s in Unity, as they are objects that define tangible\n \n things\n \n inside your game. These Entities are simply containers for the\n \n Components\n \n that are attached to them (such as\n \n positionComponent, healthComponent, movementComponent\n \n , etc.). Where this structure differs from Unity’s standard models (and OOP design generally), is that neither Entities nor Components have any behaviour defined on them – they contain no class methods, only dumb data as member variables.\n
\n
\n Rather, all the behaviour that acts on Entities and their Components is contained inside\n \n Engines\n \n (also known in some ECS frameworks as\n \n Systems\n \n ). Inside Engines, all of the methods that define how certain Entities should behave are executed. In this way, rather than having each object with a movement script executing its own movement function to move, in ECS a single MovementEngine acts on all Entities with MovementComponents to create the same result.\n
\n
\n The final key concept found in Svelto.ECS is the idea of\n \n EntityViews\n \n . An EntityView is simply a wrapper around each Entity that Engines use to interact with different Entities in a polymorphic way. Several of these EntityView wrappers can be defined for each Entity depending on the different roles the Entity might play in each engine.\n
\n
\n For a tangible example, in our game, the Player Entity plays several unrelated roles: Firstly, it needs to store data about player input created by the\n \n PlayerInput\n \n Engine to indicate where the player is aiming their gun. It also, however, needs to store a position for the\n \n ZombieMovement\n \n Engine to read to cause all Zombies to move towards the player’s position. These two roles do not (and really should not) need to interact whatsoever, which is why the Player Entity has two EntityViews (EV) defined:\n \n PlayerEV\n \n and\n \n ZombieDestinationEV\n \n . These two EntityViews, despite referring to the same Entity under the hood, each contain references only to those Components that are relevant to their one purpose.\n
\n
\n The diagram below shows the structure of our game that we will cover in this tutorial.\n
\n
\n \n
\n
\n If all of that sounds very abstract, then let’s get started making a game and make things a little more tangible. We’ll explain concepts in more detail as they become relevant.\n
\n
\n Shooting guns and reading input\n
\n
\n Download\n \n the project skeleton from GitHub\n \n and open it in Unity. This project includes a copy of Svelto.ECS (and its utility libraries\n \n Svelto.Tasks\n \n and\n \n Svelto.Common\n \n ), as well as a half-finished game structure.\n
\n
\n All Entities, Components and Engines necessary for the game have been defined already (according to the diagram above), but it’s up to us to write the Engines’ behaviour so they do what we want them to.\n
\n
\n (Have a look around the project to get a feel for it if you’d like. Notice that Entities themselves are defined only in tiny little class definitions (\n \n EntityDescriptors\n \n ), whereas their respective EntityViews are what define which Components each of them actually hold. You may also notice the mysterious\n \n Implementor\n \n classes, which we will cover later – though the astute reader can probably gather what purpose they serve with a little nosing around. If you’re curious about how all Entities and Engines are initialised, have a peek in the\n \n MainContext.cs\n \n file (which is attached to the\n \n GameContext\n \n GameObject in the Unity scene).)\n
\n
\n Let’s begin by taking a look at the\n \n GunShootingEngine\n \n (\n \n Assets/Scripts/Engines/Player/GunShootingEngine.cs\n \n ) in Visual Studio (or any code editor of your choice). As the name implies, this class will control the shooting behaviour of the Player’s gun.\n \n
\n
\n You’ll notice that\n \n GunShootingEngine\n \n is a\n \n “SingleEntityViewEngine”\n \n for the\n \n PlayerEV\n \n , which simply means it will receive a reference to\n \n PlayerEV\n \n when the game begins through its\n \n Add\n \n method (where you can see we’re storing the reference as a member variable for safekeeping).\n
\n
\n \n GunShootingEngine\n \n is not, however, a\n \n MonoBehaviour\n \n , which means it will not have any\n \n Awake, Start\n \n or\n \n Update\n \n functions called automatically by Unity as we might be used to. How, then, are we going to ensure this Engine is updated every frame? With a\n \n coroutine\n \n .\n
\n
\n We’ll start by defining our coroutine in the usual way as a method that returns an IEnumerator. Every time this method updates, it will check to see whether the Player wants to fire their gun, so name it accordingly.\n
\n
\n \n
\n
\n Inside our new coroutine, we will very simply loop forever, checking at every iteration whether the value of\n \n isFiring\n \n inside our\n \n PlayerEV\n \n ’s\n \n inputComponent\n \n is set to\n \n true\n \n . Being wary of infinite loops, we remember to\n \n yield return null\n \n at the end of our loop to make sure it only continues its next iteration in the next frame.\n
\n \n If\n \n isFiring\n \n is indeed true, we will call a wishfully defined\n \n Shoot\n \n function – you can go ahead and define that now (as a simple void no-argument function), or simply let Visual Studio auto-generate it for you (move your cursor to your function call and press ALT+ENTER).\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n
\n
\n
\n Before we implement our new function, let’s satisfy one likely point of curiosity: Where is the value of\n \n isFiring\n \n being set? A very good question we should address right away. Let’s hop over to\n \n PlayerInputEngine\n \n (\n \n Assets/Scripts/Engines/Player/PlayerInputEngine.cs\n \n ) and make sure it’s doing what we want it to do before we proceed.\n
\n
\n \n
\n
\n \n PlayerInputEngine\n \n is a\n \n SingleEntityViewEngine\n \n for the\n \n PlayerEV\n \n as well. The purpose of this engine will be to continuously read input from the Player’s mouse and store these as values in the\n \n PlayerEV\n \n ’s\n \n inputComponent\n \n .\n
\n
\n (You can have a look at the definition of the Player’s\n \n inputComponent\n \n (\n \n Assets/Scripts/Components/Player/PlayerInputComponent.cs\n \n ) to get a sense of the values this engine will modify.)\n
\n
\n Let’s make a coroutine to do this input reading; as per usual, we’ll want an infinite loop inside it that yields at the end of each iteration.\n
\n
\n \n
\n
\n There are three\n \n inputComponent\n \n values that we want to update here:\n \n aimPos\n \n (which is the screen space position of the mouse),\n \n isFiring\n \n (which is a boolean indicating whether the Player clicked their primary mouse button this frame) and\n \n aimRay\n \n (which will be a Unity Ray that points straight forward from the Player’s mouse position into world space).\n
\n
\n For the first two, we’ll simply take Unity’s\n \n Input.Position\n \n and\n \n Input.GetButtonDown(“Fire1”)\n \n values and store them as-is. Then we’ll use a function of our member variable\n \n m_Camera\n \n (which is simply a wrapper around Unity’s camera utility functions),\n \n \n ScreenPointToRay\n \n \n , to convert the Player’s mouse position into a Ray for us to store.\n
\n
\n \n
\n
\n Finally, we’ll call our coroutine as soon as the Engine boots up inside the\n \n Add\n \n function. Again, since our Engines are not\n \n MonoBehaviour\n \n s, we don’t have access to Unity’s\n \n StartCoroutine\n \n function, but the Svelto.ECS utility library Svelto.Tasks lets us do exactly the same thing (in fact, even more efficiently!) by simply typing\n \n ReadInput().Run()\n \n .\n
\n
\n \n
\n
\n With Player Input neatly sorted, let’s return to\n \n GunShootingEngine\n \n to complete it. now that we know we’re reading accurate input values.\n
\n
\n Inside our\n \n Shoot\n \n function in\n \n GunShootingEngine\n \n , we want to do three things: Check whether the Ray from the Player’s mouse is touching a Collider object, check whether that object is a Zombie, and – if it is, decrement its health. We’ll also want to store a reference to the point of impact in a\n \n Vector3\n \n for reasons that will become clear presently. Feel free to copy the code below, or implement it gradually as we go through it.\n
\n
\n \n
\n
\n The\n \n m_RayCaster\n \n member object is just another wrapper around Unity’s RayCasting functionality, and its function\n \n GetRayHitTarget\n \n will return the Entity ID of the first Entity that was hit by the Player’s Ray, or -1 if nothing was hit.\n
\n
\n If we did in fact hit something, we make use of our\n \n IEntityViewsDatabase\n \n property, which (as the name implies) is a database of all the EntityViews currently alive in the scene.\n
\n
\n (Where did this property magically appear from? Notice that\n \n GunShootingEngine\n \n implements the eloquently named\n \n IQueryingEntityViewEngine\n \n interface. All engines that implement this interface will automatically have a reference to this database assigned to them when Svelto.ECS initialises. It will quickly become apparent that these databases are crucial for engines to function properly.)\n
\n
\n With this EntityView database, we check to see whether the Entity we hit is a\n \n GunTargetEV\n \n using the\n \n TryQueryEntityView\n \n function.\n
\n
\n (Recall from the diagram earlier that\n \n GunTargetEV\n \n is really just a synonym for the Zombie Entity. (Or, more accurately,\n \n GunTargetEV\n \n is one of the EntityViews that define what a Zombie is.))\n
\n
\n If it is, we’ll access that GunTarget’s\n \n healthComponent\n \n , and decrease its\n \n currentHealth\n \n value by the\n \n damagePerShot\n \n value defined in our\n \n PlayerEV’s\n \n \n gunComponent\n \n (this is just a simple\n \n int\n \n value currently set to 1 that we can customise later). Finally, we’ll store the position of our\n \n impactPoint\n \n (the point where our bullet touched the Zombie) in the\n \n lastImpactPos\n \n value of our\n \n PlayerEV’s gunComponent\n \n .\n
\n
\n Remembering to start our\n \n CheckForFire\n \n coroutine right after our\n \n PlayerEV\n \n reference has been assigned, we now have functioning gun shooting behaviour in our game. With a little bit of sound and visual effects we’ll be able to see the impact of this Engine.\n
\n
\n \n
\n
\n Now, if you diligently copied the lines of code into\n \n Shoot\n \n from above, you might still be curious why the two variables we assigned to at the end of\n \n Shoot\n \n have a\n \n .value\n \n property attached at the end, and what this could be useful for. Rightfully so – let’s explore that right away.\n
\n
\n Broadcasting messages and splattering blood\n
\n
\n If you take a look at the definition of\n \n PlayerEV’s gunComponent\n \n (\n \n Assets/Scripts/Components/Player/GunComponent.cs\n \n ), you’ll notice that\n \n lastImpactPos\n \n is not just a regular\n \n Vector3\n \n , but actually a\n \n “DispatchOnSet<Vector3>”\n \n .\n
\n
\n This\n \n DispatchOnSet\n \n data type actually represents one of the primary means of engines communicating with each other in Svelto.ECS.\n
\n
\n (There are actually several useful methods for communicating across classes in Svelto that are equally modular (and I would encourage you to try them out!), but for the sake of simplicity we will only use\n \n DispatchOnSet\n \n in this tutorial).\n
\n
\n \n DispatchOnSet\n \n is a wrapper around a simple value-type variable, but is also a self-contained Observer-Listener object. Any class that can access a\n \n DispatchOnSet\n \n property can sign up callback functions to be notified whenever the value it refers to is changed.\n
\n
\n So, in the case of our\n \n lastImpactPos\n \n variable, the moment we changed its value inside\n \n GunShootingEngine\n \n , every class that signed up to listen for that change would be immediately notified. At the moment, however,\n \n lastImpactPos\n \n has no subscribers, so let’s do something about that.\n
\n
\n Navigate to the\n \n GunEffectsEngine\n \n (\n \n Assets/Engines/Player/GunEffectsEngine.cs\n \n ). This engine, too, works on the single EntityView\n \n PlayerEV\n \n . Inside here we’re going to instantiate a simple blood splattering particle effect whenever the player’s gun successfully shoots a zombie.\n
\n
\n \n
\n
\n Inside the Add function, right after we’ve been assigned a reference to\n \n PlayerEV\n \n , we’ll sign up to be notified of changes to\n \n lastImpactPos\n \n . To do this, simply access\n \n PlayerEV\n \n ’s\n \n gunComponent\n \n , and call\n \n lastImpactPos’s\n \n \n NotifyOnValueSet\n \n function, passing in a wishfully titled callback function name.\n
\n
\n \n
\n
\n Again, you can let Visual Studio auto-generate an appropriate function for this purpose (simply select the function name you passed in and hit ALT+ENTER).\n
\n
\n \n
\n
\n As you’ll note from Visual Studio’s auto-generated parameters (which we can rename once we know what they refer to), this function will take in an\n \n int\n \n , which is the ID of the Entity that holds the value that was just changed, and the changed value itself (in this case, a\n \n Vector3\n \n ).\n
\n
\n If you recall from your look at the\n \n gunComponent\n \n earlier, it already contains a\n \n gunEffectPrefab\n \n variable (where this has been assigned we will cover in just a moment). Let’s retrieve that and instantiate it at the gun’s impact position.\n
\n
\n \n
\n
\n We instantiate our prefab using the\n \n Build\n \n function of our member object\n \n m_GameObjectFactory\n \n .\n
\n
\n (\n \n GameObjectFactory\n \n is (for our purposes, anyway) little more than a wrapper around Unity’s familiar\n \n Instantiate\n \n function that is passed in manually through\n \n GunEffectsEngine\n \n ’s constructor\n \n \n – check out\n \n MainContext.cs\n \n to see where the construction of this object happens.)\n
\n
\n With the prefab instantiated, we assign its position to the new bullet impact position that we just had passed in.\n
\n
\n With that, we now have bullets in the game that splatter blood particles on impact (you can check and tweak the blood effect by inspecting the prefab (\n \n Assets/Prefabs/Blood.prefab\n \n )). Of course, until we have Zombies spawning, we have little to test this functionality against.\n
\n
\n Before we proceed with developing our Zombie-spawning functionality, however, let’s take a moment to unwrap the final key piece of Svelto.ECS that we’ve only skimmed over up to this point:\n \n Implementors\n \n .\n
\n
\n So far, we’ve accessed Components belonging to several different Entities, using them both to read constant data (such as\n \n damagePerShot\n \n and\n \n bloodEffectPrefab\n \n ), and to edit data for other Engines to read (such as\n \n currentHealth\n \n and\n \n lastImpactPos\n \n ).\n
\n
\n If you’ve inspected the definitions of these Components, you’ll have seen that they are simply interfaces that declare the existence of these properties. This is another deliberate design decision to keep Svelto.ECS codebases as modular as possible, but it raises the immediate practical question – where are they implemented? Investigate the\n \n Assets/Scripts/Implementors\n \n folder (and its subfolders) to get a clear answer.\n
\n
\n \n
\n
\n These\n \n Implementor\n \n classes provide specific class implementations of every Component in the game. While some of them (notably\n \n PlayerInputImplementor\n \n ) are very straightforward classes that simply define the properties laid out in their interface, others (\n \n GunImplementor\n \n , and several of the Zombie Implementors which we’ll address in a moment) are subclasses of\n \n MonoBehaviour\n \n as well, and contain some interaction with Unity.\n
\n
\n Implementors are where these traditional Unity interactions can take place (such as receiving a\n \n OnTriggerEnter\n \n callback or receiving a\n \n SerializeField\n \n property from the Unity Inspector).\n
\n
\n (Notice, for example, that the\n \n bloodEffectPrefab\n \n we accessed in\n \n GunEffectsEngine\n \n is simply a serialised GameObject variable that has been dragged in from the\n \n Assets/Prefabs\n \n folder onto the\n \n GunImplementor\n \n MonoBehaviour script attached to the\n \n Player Camera\n \n object in our Unity scene.)\n
\n
\n Implementors, in other words, are the bridge between Unity and Svelto.ECS, allowing the rest of the codebase to be (mostly) independent from how Unity works.\n
\n
\n With that little aside, let’s return to our Engine development, with a mind towards getting Zombies spawning in the game.\n
\n
\n Spawning zombies and making them move\n
\n
\n If you’ve looked around our Unity scene, you’ll have seen there’s a\n \n ZombieSpawner\n \n object with a number of empty child transforms called\n \n SpawnPosition\n \n . These are the places we’ll want our Zombies to spawn from.\n
\n
\n Open the\n \n ZombieSpawnerEngine\n \n (\n \n Assets/Scripts/Engines/Zombies/ZombieSpawnerEngine.cs\n \n ), and you’ll the same basic structure as the other engines we’ve looked at.\n
\n
\n (You can inspect the definition of the ZombieSpawnerEV, and its\n \n zombieSpawnerComponent\n \n to get an idea of the data we’ll be retrieving and manipulating here).\n
\n
\n The purpose of this engine is very simply to spawn a new zombie entity every few seconds. Let’s create another coroutine to do this.\n
\n
\n \n
\n
\n Inside this coroutine we’re going to loop infinitely, at every iteration picking a random spawn position from the spawnPosition field of our\n \n ZombieSpawnerEV\n \n ’s spawnerComponent. Finally we’ll build that same component’s\n \n zombieToSpawn\n \n prefab similar to how we’ve done before, and place it at the randomly chosen spawn position.\n
\n
\n \n
\n
\n (If you want to see how these spawn positions are retrieved, check out\n \n ZombieSpawnerImplementor\n \n . Incidentally, you may notice here that even though Components and their Implementors should ideally contain only data accessors, there are a few lines of behaviour code inside this class’s\n \n Awake\n \n function. These small snippets are sometimes necessary to keep implementation details encapsulated. It’s a fine balance.)\n
\n
\n We’re not quite done, yet, however. As you’ll have gathered, a Zombie actually represents an Entity inside our ECS structure, and as such we want to make sure that we’re building a new\n \n ZombieEntity\n \n at the same time as its Unity GameObject is spawned.\n
\n
\n We’ll achieve this by retrieving a list of all of our newly spawned Zombie’s Implementor classes (as these are all MonoBehaviours, they can be accessed with a simple\n \n GetComponents\n \n call). From there, we’ll use our\n \n EntityFactory\n \n member object to build a new Zombie entity, assigning it a unique Entity ID by simply retrieving its GameObject\n \n InstanceID\n \n .\n
\n
\n \n
\n
\n With this new Zombie Entity built, we’ll grab its ID one more time and store it in the\n \n lastSpawnedID\n \n value of our\n \n ZombieSpawnerEV\n \n ’s\n \n spawnerComponent\n \n . This\n \n lastSpawnedID\n \n variable is another DispatchOnSet property that allows us to implicitly alert other Engines that are interested in knowing about our newly spawned Zombie.\n
\n
\n \n
\n
\n (Notice in the snippet above that we’re yielding to the next frame\n \n before\n \n we assign our\n \n lastSpawnedID\n \n value. This is deliberate, as it ensures that Svelto.ECS has completely finished building the new Zombie Entity before other Engines listening for\n \n lastSpawnedID\n \n start querying for it.)\n
\n
\n Finally, we’ll want to adjust our coroutine so that it only spawns a new Zombie every few seconds (certainly not every frame). Here we’ll make use of Unity’s handy\n \n \n WaitForSeconds\n \n \n \n \n object. We’ll assign it as a member variable, and construct it once our\n \n ZombieSpawnerEV\n \n reference has been added (in the\n \n Add\n \n function), using the\n \n secsBetweenSpawns\n \n property of our\n \n ZombieSpawnerEV\n \n ’s\n \n spawnerComponent\n \n (a simple\n \n float\n \n value currently set to 3.0).\n
\n
\n \n
\n
\n \n
\n
\n We can then yield return our new\n \n WaitForSeconds\n \n object at the start of every loop iteration, to ensure a few seconds’ delay between each spawn.\n
\n
\n \n
\n
\n Finally, let’s run our coroutine at the end of the\n \n Add\n \n function.\n
\n
\n \n
\n
\n Try running the game in Unity play mode now, and you should indeed see Zombies spawning around the scene. Try to clicking on them with your mouse as well to test the Gun Engines we implemented earlier!\n
\n
\n Besides looping a simple walking animation, however, these Zombies do not move, so once the novelty of firing at these rather boring enemies subsides, let’s by proceed by implementing their movement behaviour.\n
\n
\n Open up the\n \n ZombieMovementEngine\n \n (\n \n Assets/Scripts/Engines/Zombies/ZombieMovementEngine.cs\n \n ).\n
\n
\n Inside its\n \n Add\n \n function, once our\n \n ZombieSpawnerEV\n \n reference has been assigned, we’ll sign up to changes to its\n \n lastSpawnedID\n \n . As before, let Visual Studio generate the corresponding callback function.\n
\n
\n \n
\n
\n \n
\n
\n Inside this callback function, we’ll use our\n \n EntityViewsDB\n \n to retrieve a reference to the Zombie that was just spawned (using the ID passed in as the second argument to the function).\n
\n
\n \n
\n
\n Next, we’ll access the Zombie’s\n \n movementComponent\n \n , and set\n \n navMeshEnabled\n \n to\n \n true\n \n to activate its Unity\n \n NavMeshAgent\n \n behaviour.\n
\n
\n \n
\n
\n We want to assign the Player’s position as the\n \n NavMeshDestination\n \n of this Zombie so that the Zombie will move towards the Player. To do this, however, we need to retrieve a reference to the Player Entity.\n
\n
\n Recall from the diagram earlier that the Player Entity is also defined through the\n \n ZombieDestinationEV\n \n for exactly this purpose. Because we happen to know that there will only ever be one such EntityView in our scene, we can just query for all\n \n ZombieDestinationEV\n \n s using our\n \n EntityViewsDB\n \n and grab the first one it returns.\n
\n
\n \n
\n
\n With that done, we’ll simply grab the position from our\n \n ZombieDestinationEV\n \n ’s\n \n positionComponent\n \n , and assign it as the\n \n navMeshDestination\n \n of our newly spawned Zombie.\n
\n
\n Hit play in Unity now and you should see that the Zombies suddenly look more ominous when coming straight towards you. The illusion may be lost as you see them walk straight the camera, however. Let’s sort that out next.\n
\n
\n Triggers and animations\n
\n
\n We want our Zombies to stop moving once they’re within grabbing distance of the Player.\n
\n
\n If you inspect the Unity scene, you’ll see that the\n \n Player Camera\n \n object has a\n \n Sphere Collider\n \n attached to it. The\n \n triggerComponent\n \n of each\n \n ZombieEV\n \n , meanwhile, has a\n \n DispatchOnSet<bool>\n \n property that will be set to true whenever a Zombie touches a Trigger Collider (check out\n \n ZombieTriggerImplementor\n \n to see exactly how this interfaces with the Unity collision system).\n
\n
\n Back inside\n \n ZombieMovementEngine\n \n , then, let’s stop our Zombies’ movement the moment this trigger flag has been set.\n
\n
\n We’ll start inside the callback function for a new Zombie spawn that we just wrote (here called\n \n OnZombieSpawn\n \n ), signing up to be notified of changes to our Zombie’s\n \n triggeredAgainstTarget\n \n property (on its\n \n triggerComponent\n \n ).\n
\n
\n \n
\n
\n Inside the callback function we just assigned, we’ll simply retrieve a reference to the Zombie in question and disable its\n \n navMeshAgent\n \n property.\n
\n
\n \n
\n
\n With these basic gameplay features in place, we’ll proceed to add a little more aliveness to our Zombies by triggering different animations in response to what’s happening in the game.\n
\n
\n (If you select a Zombie GameObject in Unity and view the Animator window, you will see that its animation system has been set up to respond to two triggers, which we’ll be setting in\n \n ZombieAnimationEngine\n \n . You can also inspect the\n \n ZombieAnimationImplementor\n \n to see how the interaction between Svelto.ECS and the Unity animation happens.)\n
\n Here we’re going to do exactly the same thing we did in\n \n ZombieMovementEngine\n \n , by signing up a callback against our\n \n ZombieSpawnerEV\n \n ’s\n \n lastSpawnedID\n \n property.\n
\n
\n \n
\n
\n Inside this callback, we’ll sign up two additional callback functions – one for changes to the Zombie’s health (the\n \n currentHealth\n \n property of its\n \n healthComponent\n \n ), and one for the Zombie triggering against the Player (the\n \n triggeredAgainstTarget\n \n property of its\n \n triggerComponent\n \n ).\n
\n
\n \n
\n
\n In the first of our new callback functions, we’ll check to see whether the Zombie’s current health (passed in as a parameter) is less than or equal to zero. If it is, we’ll retrieve a reference to the Zombie in question, and set the\n \n trigger\n \n property of its\n \n animationComponent\n \n to “deathTrigger”, activating its death animation.\n
\n
\n \n
\n
\n Similarly, in the second callback function, we’ll set the same\n \n trigger\n \n value to “attackTrigger” to activate the Zombie’s attack animation once it’s touching the Player.\n
\n
\n \n
\n
\n The final touch we’ll want to add here before we finish (as should become apparent with a little playtesting), is to disable each Zombie’s movement and trigger behaviour once their health has dropped to zero. Hop back to\n \n ZombieMovementEngine\n \n and make this final adjustment.\n
\n
\n \n
\n
\n \n
\n
\n *\n
\n
\n With that, your Zombies should now be aggressively chasing you down, yet fall over graciously when shot at enough times.\n
\n
\n Hopefully this short tutorial has given you a sense of some of the key concepts underlying game development around ECS. In the next tutorial, we’ll expand our little game to include a GUI with a scoring system, some additional sound effects and a final game over condition.\n
\n \n","kudosSumWeight":1,"postTime":"2019-03-21T12:53:05.112-07:00","images":{"__typename":"AssociatedImageConnection","edges":[{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDE","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNjlpRDcwQUFGOTlEM0RFQTRGQQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDI","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzBpMUExNUM4MjU3NjVEOTMxOA?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDM","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzFpNjQzMTY1MEM5M0VBNzg4MA?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDQ","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzJpNUYwQzAyREQwNEYwMjI5NQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDU","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzNpMEMxMjIxOTk2QkZCMTQxMg?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDY","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzRpQzJCNTdBQzU3MjlEMTFENg?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDc","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzVpOEJBMjQzMzA2QzEyMTU1Qw?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDg","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzZpRjc3QTAwQjQyNEU5NDc4QQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDk","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzdpOEIwOEY0QzIyMTc5MzdDMQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDEw","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzhpRkVGMjVGRUQxNTQxNzJGRg?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDEx","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzlpM0FCRkNFQTkzM0E5MjNGRA?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDEy","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODBpNjkyNzhCNEZBRUYxQTFDMg?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDEz","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODFpREUyMjJFRTFCODIzRjg2NQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDE0","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODRpMTY4OEYxQTg1QjdFNkEzRA?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDE1","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODZpQUEwMjc2RjI5OUZCODM0NA?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDE2","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODhpMTFFRkI5RkNDMkU0QkUzRQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDE3","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTBpNUVFQzRFNTE4RDNBNjY3NA?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDE4","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTJpNzMyMjRCMDJBRkYyRDUzNg?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDE5","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTRpN0JDRTgyQjZFRkIwQzVENg?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDIw","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTVpNzkyRTJEOEU2QjMzODkzNA?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDIx","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTZpRTE0OUQyMTNCQzVGNTJFRQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDIy","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTdpNUQ3QThCNUNBOEZDQTk1RQ?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDIz","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOThpNTIyQjE3ODY4QkQxNkU0Rg?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDI0","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTlpRjdCRUQwMjg4Q0ZBMTlENw?revision=2\"}"}},{"__typename":"AssociatedImageEdge","cursor":"MjUuMXwyLjF8b3wyNXxfTlZffDI1","node":{"__ref":"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYzMDFpNjBENjhDQzA3RUI2QUVBQw?revision=2\"}"}}],"totalCount":41,"pageInfo":{"__typename":"PageInfo","hasNextPage":true,"endCursor":"MjUuMXwyLjF8b3wyNXxfTlZffDI1","hasPreviousPage":false,"startCursor":null}},"attachments":{"__typename":"AttachmentConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[]},"tags":{"__typename":"TagConnection","pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null},"edges":[{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDE","node":{"__typename":"Tag","id":"tag:ecs","text":"ecs","time":"2019-03-21T12:53:08.473-07:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}},{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDI","node":{"__typename":"Tag","id":"tag:game development","text":"game development","time":"2019-03-21T04:13:41.884-07:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}},{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDM","node":{"__typename":"Tag","id":"tag:Gaming","text":"Gaming","time":"2018-06-15T14:57:36.633-07:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}},{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDQ","node":{"__typename":"Tag","id":"tag:microsoft student partner","text":"microsoft student partner","time":"2019-01-08T18:24:08.166-08:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}},{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDU","node":{"__typename":"Tag","id":"tag:UCL","text":"UCL","time":"2019-03-21T05:13:58.572-07:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}},{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDY","node":{"__typename":"Tag","id":"tag:Unity","text":"Unity","time":"2019-01-08T18:36:44.826-08:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}},{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDc","node":{"__typename":"Tag","id":"tag:unity3d","text":"unity3d","time":"2019-03-21T04:32:31.370-07:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}},{"__typename":"TagEdge","cursor":"MjUuMXwyLjF8b3wxMHxfTlZffDg","node":{"__typename":"Tag","id":"tag:visual studio","text":"visual studio","time":"2016-09-06T13:11:16.175-07:00","lastActivityTime":null,"messagesCount":null,"followersCount":null}}]},"timeToRead":16,"rawTeaser":"First published on MSDN on May 08, 2018 Guest post by Sondre Agledahl: Microsoft Student Partner, Games programmer and CS student at UCL.","introduction":"","coverImage":null,"coverImageProperties":{"__typename":"CoverImageProperties","style":"STANDARD","titlePosition":"BOTTOM","altText":""},"currentRevision":{"__ref":"Revision:revision:381053_2"},"latestVersion":{"__typename":"FriendlyVersion","major":"2","minor":"0"},"metrics":{"__typename":"MessageMetrics","views":5699},"visibilityScope":"PUBLIC","canonicalUrl":null,"seoTitle":null,"seoDescription":null,"placeholder":false,"originalMessageForPlaceholder":null,"contributors":{"__typename":"UserConnection","edges":[]},"nonCoAuthorContributors":{"__typename":"UserConnection","edges":[]},"coAuthors":{"__typename":"UserConnection","edges":[]},"blogMessagePolicies":{"__typename":"BlogMessagePolicies","canDoAuthoringActionsOnBlog":{"__typename":"PolicyResult","failureReason":{"__typename":"FailureReason","message":"error.lithium.policies.blog.action_can_do_authoring_action.accessDenied","key":"error.lithium.policies.blog.action_can_do_authoring_action.accessDenied","args":[]}}},"archivalData":null,"replies":{"__typename":"MessageConnection","edges":[],"pageInfo":{"__typename":"PageInfo","hasNextPage":false,"endCursor":null,"hasPreviousPage":false,"startCursor":null}},"customFields":[],"revisions({\"constraints\":{\"isPublished\":{\"eq\":true}},\"first\":1})":{"__typename":"RevisionConnection","totalCount":2}},"Conversation:conversation:381053":{"__typename":"Conversation","id":"conversation:381053","solved":false,"topic":{"__ref":"BlogTopicMessage:message:381053"},"lastPostingActivityTime":"2019-03-21T12:53:08.473-07:00","lastPostTime":"2019-03-21T12:53:05.112-07:00","unreadReplyCount":0,"isSubscribed":false},"ModerationData:moderation_data:381053":{"__typename":"ModerationData","id":"moderation_data:381053","status":"APPROVED","rejectReason":null,"isReportedAbuse":false,"rejectUser":null,"rejectTime":null,"rejectActorType":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNjlpRDcwQUFGOTlEM0RFQTRGQQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNjlpRDcwQUFGOTlEM0RFQTRGQQ?revision=2","title":"","associationType":"BODY","width":804,"height":361,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzBpMUExNUM4MjU3NjVEOTMxOA?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzBpMUExNUM4MjU3NjVEOTMxOA?revision=2","title":"","associationType":"BODY","width":194,"height":231,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzFpNjQzMTY1MEM5M0VBNzg4MA?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzFpNjQzMTY1MEM5M0VBNzg4MA?revision=2","title":"","associationType":"BODY","width":600,"height":450,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzJpNUYwQzAyREQwNEYwMjI5NQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzJpNUYwQzAyREQwNEYwMjI5NQ?revision=2","title":"","associationType":"BODY","width":851,"height":647,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzNpMEMxMjIxOTk2QkZCMTQxMg?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzNpMEMxMjIxOTk2QkZCMTQxMg?revision=2","title":"","associationType":"BODY","width":851,"height":438,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzRpQzJCNTdBQzU3MjlEMTFENg?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzRpQzJCNTdBQzU3MjlEMTFENg?revision=2","title":"","associationType":"BODY","width":851,"height":510,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzVpOEJBMjQzMzA2QzEyMTU1Qw?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzVpOEJBMjQzMzA2QzEyMTU1Qw?revision=2","title":"","associationType":"BODY","width":855,"height":411,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzZpRjc3QTAwQjQyNEU5NDc4QQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzZpRjc3QTAwQjQyNEU5NDc4QQ?revision=2","title":"","associationType":"BODY","width":824,"height":230,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzdpOEIwOEY0QzIyMTc5MzdDMQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzdpOEIwOEY0QzIyMTc5MzdDMQ?revision=2","title":"","associationType":"BODY","width":831,"height":221,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzhpRkVGMjVGRUQxNTQxNzJGRg?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzhpRkVGMjVGRUQxNTQxNzJGRg?revision=2","title":"","associationType":"BODY","width":830,"height":548,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzlpM0FCRkNFQTkzM0E5MjNGRA?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyNzlpM0FCRkNFQTkzM0E5MjNGRA?revision=2","title":"","associationType":"BODY","width":855,"height":678,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODBpNjkyNzhCNEZBRUYxQTFDMg?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODBpNjkyNzhCNEZBRUYxQTFDMg?revision=2","title":"","associationType":"BODY","width":840,"height":223,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODFpREUyMjJFRTFCODIzRjg2NQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODFpREUyMjJFRTFCODIzRjg2NQ?revision=2","title":"","associationType":"BODY","width":855,"height":213,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODRpMTY4OEYxQTg1QjdFNkEzRA?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODRpMTY4OEYxQTg1QjdFNkEzRA?revision=2","title":"","associationType":"BODY","width":855,"height":348,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODZpQUEwMjc2RjI5OUZCODM0NA?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODZpQUEwMjc2RjI5OUZCODM0NA?revision=2","title":"","associationType":"BODY","width":855,"height":224,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODhpMTFFRkI5RkNDMkU0QkUzRQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyODhpMTFFRkI5RkNDMkU0QkUzRQ?revision=2","title":"","associationType":"BODY","width":855,"height":437,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTBpNUVFQzRFNTE4RDNBNjY3NA?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTBpNUVFQzRFNTE4RDNBNjY3NA?revision=2","title":"","associationType":"BODY","width":855,"height":149,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTJpNzMyMjRCMDJBRkYyRDUzNg?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTJpNzMyMjRCMDJBRkYyRDUzNg?revision=2","title":"","associationType":"BODY","width":850,"height":145,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTRpN0JDRTgyQjZFRkIwQzVENg?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTRpN0JDRTgyQjZFRkIwQzVENg?revision=2","title":"","associationType":"BODY","width":855,"height":128,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTVpNzkyRTJEOEU2QjMzODkzNA?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTVpNzkyRTJEOEU2QjMzODkzNA?revision=2","title":"","associationType":"BODY","width":855,"height":366,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTZpRTE0OUQyMTNCQzVGNTJFRQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTZpRTE0OUQyMTNCQzVGNTJFRQ?revision=2","title":"","associationType":"BODY","width":855,"height":465,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTdpNUQ3QThCNUNBOEZDQTk1RQ?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTdpNUQ3QThCNUNBOEZDQTk1RQ?revision=2","title":"","associationType":"BODY","width":849,"height":250,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOThpNTIyQjE3ODY4QkQxNkU0Rg?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOThpNTIyQjE3ODY4QkQxNkU0Rg?revision=2","title":"","associationType":"BODY","width":855,"height":256,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTlpRjdCRUQwMjg4Q0ZBMTlENw?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYyOTlpRjdCRUQwMjg4Q0ZBMTlENw?revision=2","title":"","associationType":"BODY","width":855,"height":128,"altText":null},"AssociatedImage:{\"url\":\"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYzMDFpNjBENjhDQzA3RUI2QUVBQw?revision=2\"}":{"__typename":"AssociatedImage","url":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/images/bS0zODEwNTMtOTYzMDFpNjBENjhDQzA3RUI2QUVBQw?revision=2","title":"","associationType":"BODY","width":838,"height":383,"altText":null},"Revision:revision:381053_2":{"__typename":"Revision","id":"revision:381053_2","lastEditTime":"2019-03-21T12:53:08.473-07:00"},"CachedAsset:theme:customTheme1-1744326567647":{"__typename":"CachedAsset","id":"theme:customTheme1-1744326567647","value":{"id":"customTheme1","animation":{"fast":"150ms","normal":"250ms","slow":"500ms","slowest":"750ms","function":"cubic-bezier(0.07, 0.91, 0.51, 1)","__typename":"AnimationThemeSettings"},"avatar":{"borderRadius":"50%","collections":["default"],"__typename":"AvatarThemeSettings"},"basics":{"browserIcon":{"imageAssetName":"favicon-1730836283320.png","imageLastModified":"1730836286415","__typename":"ThemeAsset"},"customerLogo":{"imageAssetName":"favicon-1730836271365.png","imageLastModified":"1730836274203","__typename":"ThemeAsset"},"maximumWidthOfPageContent":"1300px","oneColumnNarrowWidth":"800px","gridGutterWidthMd":"30px","gridGutterWidthXs":"10px","pageWidthStyle":"WIDTH_OF_BROWSER","__typename":"BasicsThemeSettings"},"buttons":{"borderRadiusSm":"3px","borderRadius":"3px","borderRadiusLg":"5px","paddingY":"5px","paddingYLg":"7px","paddingYHero":"var(--lia-bs-btn-padding-y-lg)","paddingX":"12px","paddingXLg":"16px","paddingXHero":"60px","fontStyle":"NORMAL","fontWeight":"700","textTransform":"NONE","disabledOpacity":0.5,"primaryTextColor":"var(--lia-bs-white)","primaryTextHoverColor":"var(--lia-bs-white)","primaryTextActiveColor":"var(--lia-bs-white)","primaryBgColor":"var(--lia-bs-primary)","primaryBgHoverColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) * 0.85))","primaryBgActiveColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) * 0.7))","primaryBorder":"1px solid transparent","primaryBorderHover":"1px solid transparent","primaryBorderActive":"1px solid transparent","primaryBorderFocus":"1px solid var(--lia-bs-white)","primaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","secondaryTextColor":"var(--lia-bs-gray-900)","secondaryTextHoverColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.95))","secondaryTextActiveColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.9))","secondaryBgColor":"var(--lia-bs-gray-200)","secondaryBgHoverColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.96))","secondaryBgActiveColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.92))","secondaryBorder":"1px solid transparent","secondaryBorderHover":"1px solid transparent","secondaryBorderActive":"1px solid transparent","secondaryBorderFocus":"1px solid transparent","secondaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","tertiaryTextColor":"var(--lia-bs-gray-900)","tertiaryTextHoverColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.95))","tertiaryTextActiveColor":"hsl(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), calc(var(--lia-bs-gray-900-l) * 0.9))","tertiaryBgColor":"transparent","tertiaryBgHoverColor":"transparent","tertiaryBgActiveColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.04)","tertiaryBorder":"1px solid transparent","tertiaryBorderHover":"1px solid hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","tertiaryBorderActive":"1px solid transparent","tertiaryBorderFocus":"1px solid transparent","tertiaryBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","destructiveTextColor":"var(--lia-bs-danger)","destructiveTextHoverColor":"hsl(var(--lia-bs-danger-h), var(--lia-bs-danger-s), calc(var(--lia-bs-danger-l) * 0.95))","destructiveTextActiveColor":"hsl(var(--lia-bs-danger-h), var(--lia-bs-danger-s), calc(var(--lia-bs-danger-l) * 0.9))","destructiveBgColor":"var(--lia-bs-gray-200)","destructiveBgHoverColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.96))","destructiveBgActiveColor":"hsl(var(--lia-bs-gray-200-h), var(--lia-bs-gray-200-s), calc(var(--lia-bs-gray-200-l) * 0.92))","destructiveBorder":"1px solid transparent","destructiveBorderHover":"1px solid transparent","destructiveBorderActive":"1px solid transparent","destructiveBorderFocus":"1px solid transparent","destructiveBoxShadowFocus":"0 0 0 1px var(--lia-bs-primary), 0 0 0 4px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","__typename":"ButtonsThemeSettings"},"border":{"color":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","mainContent":"NONE","sideContent":"LIGHT","radiusSm":"3px","radius":"5px","radiusLg":"9px","radius50":"100vw","__typename":"BorderThemeSettings"},"boxShadow":{"xs":"0 0 0 1px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.08), 0 3px 0 -1px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.16)","sm":"0 2px 4px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.12)","md":"0 5px 15px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.3)","lg":"0 10px 30px hsla(var(--lia-bs-gray-900-h), var(--lia-bs-gray-900-s), var(--lia-bs-gray-900-l), 0.3)","__typename":"BoxShadowThemeSettings"},"cards":{"bgColor":"var(--lia-panel-bg-color)","borderRadius":"var(--lia-panel-border-radius)","boxShadow":"var(--lia-box-shadow-xs)","__typename":"CardsThemeSettings"},"chip":{"maxWidth":"300px","height":"30px","__typename":"ChipThemeSettings"},"coreTypes":{"defaultMessageLinkColor":"var(--lia-bs-link-color)","defaultMessageLinkDecoration":"none","defaultMessageLinkFontStyle":"NORMAL","defaultMessageLinkFontWeight":"400","defaultMessageFontStyle":"NORMAL","defaultMessageFontWeight":"400","forumColor":"#4099E2","forumFontFamily":"var(--lia-bs-font-family-base)","forumFontWeight":"var(--lia-default-message-font-weight)","forumLineHeight":"var(--lia-bs-line-height-base)","forumFontStyle":"var(--lia-default-message-font-style)","forumMessageLinkColor":"var(--lia-default-message-link-color)","forumMessageLinkDecoration":"var(--lia-default-message-link-decoration)","forumMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","forumMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","forumSolvedColor":"#148563","blogColor":"#1CBAA0","blogFontFamily":"var(--lia-bs-font-family-base)","blogFontWeight":"var(--lia-default-message-font-weight)","blogLineHeight":"1.75","blogFontStyle":"var(--lia-default-message-font-style)","blogMessageLinkColor":"var(--lia-default-message-link-color)","blogMessageLinkDecoration":"var(--lia-default-message-link-decoration)","blogMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","blogMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","tkbColor":"#4C6B90","tkbFontFamily":"var(--lia-bs-font-family-base)","tkbFontWeight":"var(--lia-default-message-font-weight)","tkbLineHeight":"1.75","tkbFontStyle":"var(--lia-default-message-font-style)","tkbMessageLinkColor":"var(--lia-default-message-link-color)","tkbMessageLinkDecoration":"var(--lia-default-message-link-decoration)","tkbMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","tkbMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","qandaColor":"#4099E2","qandaFontFamily":"var(--lia-bs-font-family-base)","qandaFontWeight":"var(--lia-default-message-font-weight)","qandaLineHeight":"var(--lia-bs-line-height-base)","qandaFontStyle":"var(--lia-default-message-link-font-style)","qandaMessageLinkColor":"var(--lia-default-message-link-color)","qandaMessageLinkDecoration":"var(--lia-default-message-link-decoration)","qandaMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","qandaMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","qandaSolvedColor":"#3FA023","ideaColor":"#FF8000","ideaFontFamily":"var(--lia-bs-font-family-base)","ideaFontWeight":"var(--lia-default-message-font-weight)","ideaLineHeight":"var(--lia-bs-line-height-base)","ideaFontStyle":"var(--lia-default-message-font-style)","ideaMessageLinkColor":"var(--lia-default-message-link-color)","ideaMessageLinkDecoration":"var(--lia-default-message-link-decoration)","ideaMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","ideaMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","contestColor":"#FCC845","contestFontFamily":"var(--lia-bs-font-family-base)","contestFontWeight":"var(--lia-default-message-font-weight)","contestLineHeight":"var(--lia-bs-line-height-base)","contestFontStyle":"var(--lia-default-message-link-font-style)","contestMessageLinkColor":"var(--lia-default-message-link-color)","contestMessageLinkDecoration":"var(--lia-default-message-link-decoration)","contestMessageLinkFontStyle":"ITALIC","contestMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","occasionColor":"#D13A1F","occasionFontFamily":"var(--lia-bs-font-family-base)","occasionFontWeight":"var(--lia-default-message-font-weight)","occasionLineHeight":"var(--lia-bs-line-height-base)","occasionFontStyle":"var(--lia-default-message-font-style)","occasionMessageLinkColor":"var(--lia-default-message-link-color)","occasionMessageLinkDecoration":"var(--lia-default-message-link-decoration)","occasionMessageLinkFontStyle":"var(--lia-default-message-link-font-style)","occasionMessageLinkFontWeight":"var(--lia-default-message-link-font-weight)","grouphubColor":"#333333","categoryColor":"#949494","communityColor":"#FFFFFF","productColor":"#949494","__typename":"CoreTypesThemeSettings"},"colors":{"black":"#000000","white":"#FFFFFF","gray100":"#F7F7F7","gray200":"#F7F7F7","gray300":"#E8E8E8","gray400":"#D9D9D9","gray500":"#CCCCCC","gray600":"#717171","gray700":"#707070","gray800":"#545454","gray900":"#333333","dark":"#545454","light":"#F7F7F7","primary":"#0069D4","secondary":"#333333","bodyText":"#1E1E1E","bodyBg":"#FFFFFF","info":"#409AE2","success":"#41C5AE","warning":"#FCC844","danger":"#BC341B","alertSystem":"#FF6600","textMuted":"#707070","highlight":"#FFFCAD","outline":"var(--lia-bs-primary)","custom":["#D3F5A4","#243A5E"],"__typename":"ColorsThemeSettings"},"divider":{"size":"3px","marginLeft":"4px","marginRight":"4px","borderRadius":"50%","bgColor":"var(--lia-bs-gray-600)","bgColorActive":"var(--lia-bs-gray-600)","__typename":"DividerThemeSettings"},"dropdown":{"fontSize":"var(--lia-bs-font-size-sm)","borderColor":"var(--lia-bs-border-color)","borderRadius":"var(--lia-bs-border-radius-sm)","dividerBg":"var(--lia-bs-gray-300)","itemPaddingY":"5px","itemPaddingX":"20px","headerColor":"var(--lia-bs-gray-700)","__typename":"DropdownThemeSettings"},"email":{"link":{"color":"#0069D4","hoverColor":"#0061c2","decoration":"none","hoverDecoration":"underline","__typename":"EmailLinkSettings"},"border":{"color":"#e4e4e4","__typename":"EmailBorderSettings"},"buttons":{"borderRadiusLg":"5px","paddingXLg":"16px","paddingYLg":"7px","fontWeight":"700","primaryTextColor":"#ffffff","primaryTextHoverColor":"#ffffff","primaryBgColor":"#0069D4","primaryBgHoverColor":"#005cb8","primaryBorder":"1px solid transparent","primaryBorderHover":"1px solid transparent","__typename":"EmailButtonsSettings"},"panel":{"borderRadius":"5px","borderColor":"#e4e4e4","__typename":"EmailPanelSettings"},"__typename":"EmailThemeSettings"},"emoji":{"skinToneDefault":"#ffcd43","skinToneLight":"#fae3c5","skinToneMediumLight":"#e2cfa5","skinToneMedium":"#daa478","skinToneMediumDark":"#a78058","skinToneDark":"#5e4d43","__typename":"EmojiThemeSettings"},"heading":{"color":"var(--lia-bs-body-color)","fontFamily":"Segoe UI","fontStyle":"NORMAL","fontWeight":"400","h1FontSize":"34px","h2FontSize":"32px","h3FontSize":"28px","h4FontSize":"24px","h5FontSize":"20px","h6FontSize":"16px","lineHeight":"1.3","subHeaderFontSize":"11px","subHeaderFontWeight":"500","h1LetterSpacing":"normal","h2LetterSpacing":"normal","h3LetterSpacing":"normal","h4LetterSpacing":"normal","h5LetterSpacing":"normal","h6LetterSpacing":"normal","subHeaderLetterSpacing":"2px","h1FontWeight":"var(--lia-bs-headings-font-weight)","h2FontWeight":"var(--lia-bs-headings-font-weight)","h3FontWeight":"var(--lia-bs-headings-font-weight)","h4FontWeight":"var(--lia-bs-headings-font-weight)","h5FontWeight":"var(--lia-bs-headings-font-weight)","h6FontWeight":"var(--lia-bs-headings-font-weight)","__typename":"HeadingThemeSettings"},"icons":{"size10":"10px","size12":"12px","size14":"14px","size16":"16px","size20":"20px","size24":"24px","size30":"30px","size40":"40px","size50":"50px","size60":"60px","size80":"80px","size120":"120px","size160":"160px","__typename":"IconsThemeSettings"},"imagePreview":{"bgColor":"var(--lia-bs-gray-900)","titleColor":"var(--lia-bs-white)","controlColor":"var(--lia-bs-white)","controlBgColor":"var(--lia-bs-gray-800)","__typename":"ImagePreviewThemeSettings"},"input":{"borderColor":"var(--lia-bs-gray-600)","disabledColor":"var(--lia-bs-gray-600)","focusBorderColor":"var(--lia-bs-primary)","labelMarginBottom":"10px","btnFontSize":"var(--lia-bs-font-size-sm)","focusBoxShadow":"0 0 0 3px hsla(var(--lia-bs-primary-h), var(--lia-bs-primary-s), var(--lia-bs-primary-l), 0.2)","checkLabelMarginBottom":"2px","checkboxBorderRadius":"3px","borderRadiusSm":"var(--lia-bs-border-radius-sm)","borderRadius":"var(--lia-bs-border-radius)","borderRadiusLg":"var(--lia-bs-border-radius-lg)","formTextMarginTop":"4px","textAreaBorderRadius":"var(--lia-bs-border-radius)","activeFillColor":"var(--lia-bs-primary)","__typename":"InputThemeSettings"},"loading":{"dotDarkColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.2)","dotLightColor":"hsla(var(--lia-bs-white-h), var(--lia-bs-white-s), var(--lia-bs-white-l), 0.5)","barDarkColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.06)","barLightColor":"hsla(var(--lia-bs-white-h), var(--lia-bs-white-s), var(--lia-bs-white-l), 0.4)","__typename":"LoadingThemeSettings"},"link":{"color":"var(--lia-bs-primary)","hoverColor":"hsl(var(--lia-bs-primary-h), var(--lia-bs-primary-s), calc(var(--lia-bs-primary-l) - 10%))","decoration":"none","hoverDecoration":"underline","__typename":"LinkThemeSettings"},"listGroup":{"itemPaddingY":"15px","itemPaddingX":"15px","borderColor":"var(--lia-bs-gray-300)","__typename":"ListGroupThemeSettings"},"modal":{"contentTextColor":"var(--lia-bs-body-color)","contentBg":"var(--lia-bs-white)","backgroundBg":"var(--lia-bs-black)","smSize":"440px","mdSize":"760px","lgSize":"1080px","backdropOpacity":0.3,"contentBoxShadowXs":"var(--lia-bs-box-shadow-sm)","contentBoxShadow":"var(--lia-bs-box-shadow)","headerFontWeight":"700","__typename":"ModalThemeSettings"},"navbar":{"position":"FIXED","background":{"attachment":null,"clip":null,"color":"var(--lia-bs-white)","imageAssetName":"","imageLastModified":"0","origin":null,"position":"CENTER_CENTER","repeat":"NO_REPEAT","size":"COVER","__typename":"BackgroundProps"},"backgroundOpacity":0.8,"paddingTop":"15px","paddingBottom":"15px","borderBottom":"1px solid var(--lia-bs-border-color)","boxShadow":"var(--lia-bs-box-shadow-sm)","brandMarginRight":"30px","brandMarginRightSm":"10px","brandLogoHeight":"30px","linkGap":"10px","linkJustifyContent":"flex-start","linkPaddingY":"5px","linkPaddingX":"10px","linkDropdownPaddingY":"9px","linkDropdownPaddingX":"var(--lia-nav-link-px)","linkColor":"var(--lia-bs-body-color)","linkHoverColor":"var(--lia-bs-primary)","linkFontSize":"var(--lia-bs-font-size-sm)","linkFontStyle":"NORMAL","linkFontWeight":"400","linkTextTransform":"NONE","linkLetterSpacing":"normal","linkBorderRadius":"var(--lia-bs-border-radius-sm)","linkBgColor":"transparent","linkBgHoverColor":"transparent","linkBorder":"none","linkBorderHover":"none","linkBoxShadow":"none","linkBoxShadowHover":"none","linkTextBorderBottom":"none","linkTextBorderBottomHover":"none","dropdownPaddingTop":"10px","dropdownPaddingBottom":"15px","dropdownPaddingX":"10px","dropdownMenuOffset":"2px","dropdownDividerMarginTop":"10px","dropdownDividerMarginBottom":"10px","dropdownBorderColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","controllerBgHoverColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.1)","controllerIconColor":"var(--lia-bs-body-color)","controllerIconHoverColor":"var(--lia-bs-body-color)","controllerTextColor":"var(--lia-nav-controller-icon-color)","controllerTextHoverColor":"var(--lia-nav-controller-icon-hover-color)","controllerHighlightColor":"hsla(30, 100%, 50%)","controllerHighlightTextColor":"var(--lia-yiq-light)","controllerBorderRadius":"var(--lia-border-radius-50)","hamburgerColor":"var(--lia-nav-controller-icon-color)","hamburgerHoverColor":"var(--lia-nav-controller-icon-color)","hamburgerBgColor":"transparent","hamburgerBgHoverColor":"transparent","hamburgerBorder":"none","hamburgerBorderHover":"none","collapseMenuMarginLeft":"20px","collapseMenuDividerBg":"var(--lia-nav-link-color)","collapseMenuDividerOpacity":0.16,"__typename":"NavbarThemeSettings"},"pager":{"textColor":"var(--lia-bs-link-color)","textFontWeight":"var(--lia-font-weight-md)","textFontSize":"var(--lia-bs-font-size-sm)","__typename":"PagerThemeSettings"},"panel":{"bgColor":"var(--lia-bs-white)","borderRadius":"var(--lia-bs-border-radius)","borderColor":"var(--lia-bs-border-color)","boxShadow":"none","__typename":"PanelThemeSettings"},"popover":{"arrowHeight":"8px","arrowWidth":"16px","maxWidth":"300px","minWidth":"100px","headerBg":"var(--lia-bs-white)","borderColor":"var(--lia-bs-border-color)","borderRadius":"var(--lia-bs-border-radius)","boxShadow":"0 0.5rem 1rem hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.15)","__typename":"PopoverThemeSettings"},"prism":{"color":"#000000","bgColor":"#f5f2f0","fontFamily":"var(--font-family-monospace)","fontSize":"var(--lia-bs-font-size-base)","fontWeightBold":"var(--lia-bs-font-weight-bold)","fontStyleItalic":"italic","tabSize":2,"highlightColor":"#b3d4fc","commentColor":"#62707e","punctuationColor":"#6f6f6f","namespaceOpacity":"0.7","propColor":"#990055","selectorColor":"#517a00","operatorColor":"#906736","operatorBgColor":"hsla(0, 0%, 100%, 0.5)","keywordColor":"#0076a9","functionColor":"#d3284b","variableColor":"#c14700","__typename":"PrismThemeSettings"},"rte":{"bgColor":"var(--lia-bs-white)","borderRadius":"var(--lia-panel-border-radius)","boxShadow":" var(--lia-panel-box-shadow)","customColor1":"#bfedd2","customColor2":"#fbeeb8","customColor3":"#f8cac6","customColor4":"#eccafa","customColor5":"#c2e0f4","customColor6":"#2dc26b","customColor7":"#f1c40f","customColor8":"#e03e2d","customColor9":"#b96ad9","customColor10":"#3598db","customColor11":"#169179","customColor12":"#e67e23","customColor13":"#ba372a","customColor14":"#843fa1","customColor15":"#236fa1","customColor16":"#ecf0f1","customColor17":"#ced4d9","customColor18":"#95a5a6","customColor19":"#7e8c8d","customColor20":"#34495e","customColor21":"#000000","customColor22":"#ffffff","defaultMessageHeaderMarginTop":"40px","defaultMessageHeaderMarginBottom":"20px","defaultMessageItemMarginTop":"0","defaultMessageItemMarginBottom":"10px","diffAddedColor":"hsla(170, 53%, 51%, 0.4)","diffChangedColor":"hsla(43, 97%, 63%, 0.4)","diffNoneColor":"hsla(0, 0%, 80%, 0.4)","diffRemovedColor":"hsla(9, 74%, 47%, 0.4)","specialMessageHeaderMarginTop":"40px","specialMessageHeaderMarginBottom":"20px","specialMessageItemMarginTop":"0","specialMessageItemMarginBottom":"10px","__typename":"RteThemeSettings"},"tags":{"bgColor":"var(--lia-bs-gray-200)","bgHoverColor":"var(--lia-bs-gray-400)","borderRadius":"var(--lia-bs-border-radius-sm)","color":"var(--lia-bs-body-color)","hoverColor":"var(--lia-bs-body-color)","fontWeight":"var(--lia-font-weight-md)","fontSize":"var(--lia-font-size-xxs)","textTransform":"UPPERCASE","letterSpacing":"0.5px","__typename":"TagsThemeSettings"},"toasts":{"borderRadius":"var(--lia-bs-border-radius)","paddingX":"12px","__typename":"ToastsThemeSettings"},"typography":{"fontFamilyBase":"Segoe UI","fontStyleBase":"NORMAL","fontWeightBase":"400","fontWeightLight":"300","fontWeightNormal":"400","fontWeightMd":"500","fontWeightBold":"700","letterSpacingSm":"normal","letterSpacingXs":"normal","lineHeightBase":"1.5","fontSizeBase":"16px","fontSizeXxs":"11px","fontSizeXs":"12px","fontSizeSm":"14px","fontSizeLg":"20px","fontSizeXl":"24px","smallFontSize":"14px","customFonts":[{"source":"SERVER","name":"Segoe UI","styles":[{"style":"NORMAL","weight":"400","__typename":"FontStyleData"},{"style":"NORMAL","weight":"300","__typename":"FontStyleData"},{"style":"NORMAL","weight":"600","__typename":"FontStyleData"},{"style":"NORMAL","weight":"700","__typename":"FontStyleData"},{"style":"ITALIC","weight":"400","__typename":"FontStyleData"}],"assetNames":["SegoeUI-normal-400.woff2","SegoeUI-normal-300.woff2","SegoeUI-normal-600.woff2","SegoeUI-normal-700.woff2","SegoeUI-italic-400.woff2"],"__typename":"CustomFont"},{"source":"SERVER","name":"MWF Fluent Icons","styles":[{"style":"NORMAL","weight":"400","__typename":"FontStyleData"}],"assetNames":["MWFFluentIcons-normal-400.woff2"],"__typename":"CustomFont"}],"__typename":"TypographyThemeSettings"},"unstyledListItem":{"marginBottomSm":"5px","marginBottomMd":"10px","marginBottomLg":"15px","marginBottomXl":"20px","marginBottomXxl":"25px","__typename":"UnstyledListItemThemeSettings"},"yiq":{"light":"#ffffff","dark":"#000000","__typename":"YiqThemeSettings"},"colorLightness":{"primaryDark":0.36,"primaryLight":0.74,"primaryLighter":0.89,"primaryLightest":0.95,"infoDark":0.39,"infoLight":0.72,"infoLighter":0.85,"infoLightest":0.93,"successDark":0.24,"successLight":0.62,"successLighter":0.8,"successLightest":0.91,"warningDark":0.39,"warningLight":0.68,"warningLighter":0.84,"warningLightest":0.93,"dangerDark":0.41,"dangerLight":0.72,"dangerLighter":0.89,"dangerLightest":0.95,"__typename":"ColorLightnessThemeSettings"},"localOverride":false,"__typename":"Theme"},"localOverride":false},"CachedAsset:text:en_US-components/common/EmailVerification-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/common/EmailVerification-1745505309750","value":{"email.verification.title":"Email Verification Required","email.verification.message.update.email":"To participate in the community, you must first verify your email address. The verification email was sent to {email}. To change your email, visit My Settings.","email.verification.message.resend.email":"To participate in the community, you must first verify your email address. The verification email was sent to {email}. Resend email."},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/Loading/LoadingDot-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/Loading/LoadingDot-1745505309750","value":{"title":"Loading..."},"localOverride":false},"CachedAsset:quilt:o365.prod:pages/blogs/BlogMessagePage:board:EducatorDeveloperBlog-1745502712875":{"__typename":"CachedAsset","id":"quilt:o365.prod:pages/blogs/BlogMessagePage:board:EducatorDeveloperBlog-1745502712875","value":{"id":"BlogMessagePage","container":{"id":"Common","headerProps":{"backgroundImageProps":null,"backgroundColor":null,"addComponents":null,"removeComponents":["community.widget.bannerWidget"],"componentOrder":null,"__typename":"QuiltContainerSectionProps"},"headerComponentProps":{"community.widget.breadcrumbWidget":{"disableLastCrumbForDesktop":false}},"footerProps":null,"footerComponentProps":null,"items":[{"id":"blog-article","layout":"ONE_COLUMN","bgColor":null,"showTitle":null,"showDescription":null,"textPosition":null,"textColor":null,"sectionEditLevel":"LOCKED","bgImage":null,"disableSpacing":null,"edgeToEdgeDisplay":null,"fullHeight":null,"showBorder":null,"__typename":"OneColumnQuiltSection","columnMap":{"main":[{"id":"blogs.widget.blogArticleWidget","className":"lia-blog-container","props":null,"__typename":"QuiltComponent"}],"__typename":"OneSectionColumns"}},{"id":"section-1729184836777","layout":"MAIN_SIDE","bgColor":"transparent","showTitle":false,"showDescription":false,"textPosition":"CENTER","textColor":"var(--lia-bs-body-color)","sectionEditLevel":null,"bgImage":null,"disableSpacing":null,"edgeToEdgeDisplay":null,"fullHeight":null,"showBorder":null,"__typename":"MainSideQuiltSection","columnMap":{"main":[],"side":[],"__typename":"MainSideSectionColumns"}}],"__typename":"QuiltContainer"},"__typename":"Quilt","localOverride":false},"localOverride":false},"CachedAsset:text:en_US-pages/blogs/BlogMessagePage-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-pages/blogs/BlogMessagePage-1745505309750","value":{"title":"{contextMessageSubject} | {communityTitle}","errorMissing":"This blog post cannot be found","name":"Blog Message Page","section.blog-article.title":"Blog Post","archivedMessageTitle":"This Content Has Been Archived","section.section-1729184836777.title":"","section.section-1729184836777.description":"","section.CncIde.title":"Blog Post","section.tifEmD.description":"","section.tifEmD.title":""},"localOverride":false},"CachedAsset:quiltWrapper:o365.prod:Common:1745505310171":{"__typename":"CachedAsset","id":"quiltWrapper:o365.prod:Common:1745505310171","value":{"id":"Common","header":{"backgroundImageProps":{"assetName":null,"backgroundSize":"COVER","backgroundRepeat":"NO_REPEAT","backgroundPosition":"CENTER_CENTER","lastModified":null,"__typename":"BackgroundImageProps"},"backgroundColor":"transparent","items":[{"id":"community.widget.navbarWidget","props":{"showUserName":true,"showRegisterLink":true,"useIconLanguagePicker":true,"useLabelLanguagePicker":true,"className":"QuiltComponent_lia-component-edit-mode__0nCcm","links":{"sideLinks":[],"mainLinks":[{"children":[],"linkType":"INTERNAL","id":"gxcuf89792","params":{},"routeName":"CommunityPage"},{"children":[],"linkType":"EXTERNAL","id":"external-link","url":"/Directory","target":"SELF"},{"children":[{"linkType":"INTERNAL","id":"microsoft365","params":{"categoryId":"microsoft365"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"windows","params":{"categoryId":"Windows"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"Common-microsoft365-copilot-link","params":{"categoryId":"Microsoft365Copilot"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-teams","params":{"categoryId":"MicrosoftTeams"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-securityand-compliance","params":{"categoryId":"microsoft-security"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"azure","params":{"categoryId":"Azure"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"Common-content_management-link","params":{"categoryId":"Content_Management"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"exchange","params":{"categoryId":"Exchange"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"windows-server","params":{"categoryId":"Windows-Server"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"outlook","params":{"categoryId":"Outlook"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-endpoint-manager","params":{"categoryId":"microsoftintune"},"routeName":"CategoryPage"},{"linkType":"EXTERNAL","id":"external-link-2","url":"/Directory","target":"SELF"}],"linkType":"EXTERNAL","id":"communities","url":"/","target":"BLANK"},{"children":[{"linkType":"INTERNAL","id":"a-i","params":{"categoryId":"AI"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"education-sector","params":{"categoryId":"EducationSector"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"partner-community","params":{"categoryId":"PartnerCommunity"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"i-t-ops-talk","params":{"categoryId":"ITOpsTalk"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"healthcare-and-life-sciences","params":{"categoryId":"HealthcareAndLifeSciences"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-mechanics","params":{"categoryId":"MicrosoftMechanics"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"public-sector","params":{"categoryId":"PublicSector"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"s-m-b","params":{"categoryId":"MicrosoftforNonprofits"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"io-t","params":{"categoryId":"IoT"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"startupsat-microsoft","params":{"categoryId":"StartupsatMicrosoft"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"driving-adoption","params":{"categoryId":"DrivingAdoption"},"routeName":"CategoryPage"},{"linkType":"EXTERNAL","id":"external-link-1","url":"/Directory","target":"SELF"}],"linkType":"EXTERNAL","id":"communities-1","url":"/","target":"SELF"},{"children":[],"linkType":"EXTERNAL","id":"external","url":"/Blogs","target":"SELF"},{"children":[],"linkType":"EXTERNAL","id":"external-1","url":"/Events","target":"SELF"},{"children":[{"linkType":"INTERNAL","id":"microsoft-learn-1","params":{"categoryId":"MicrosoftLearn"},"routeName":"CategoryPage"},{"linkType":"INTERNAL","id":"microsoft-learn-blog","params":{"boardId":"MicrosoftLearnBlog","categoryId":"MicrosoftLearn"},"routeName":"BlogBoardPage"},{"linkType":"EXTERNAL","id":"external-10","url":"https://learningroomdirectory.microsoft.com/","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-3","url":"https://docs.microsoft.com/learn/dynamics365/?WT.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-4","url":"https://docs.microsoft.com/learn/m365/?wt.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-5","url":"https://docs.microsoft.com/learn/topics/sci/?wt.mc_id=techcom_header-webpage-m365","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-6","url":"https://docs.microsoft.com/learn/powerplatform/?wt.mc_id=techcom_header-webpage-powerplatform","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-7","url":"https://docs.microsoft.com/learn/github/?wt.mc_id=techcom_header-webpage-github","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-8","url":"https://docs.microsoft.com/learn/teams/?wt.mc_id=techcom_header-webpage-teams","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-9","url":"https://docs.microsoft.com/learn/dotnet/?wt.mc_id=techcom_header-webpage-dotnet","target":"BLANK"},{"linkType":"EXTERNAL","id":"external-2","url":"https://docs.microsoft.com/learn/azure/?WT.mc_id=techcom_header-webpage-m365","target":"BLANK"}],"linkType":"INTERNAL","id":"microsoft-learn","params":{"categoryId":"MicrosoftLearn"},"routeName":"CategoryPage"},{"children":[],"linkType":"INTERNAL","id":"community-info-center","params":{"categoryId":"Community-Info-Center"},"routeName":"CategoryPage"}]},"style":{"boxShadow":"var(--lia-bs-box-shadow-sm)","controllerHighlightColor":"hsla(30, 100%, 50%)","linkFontWeight":"400","dropdownDividerMarginBottom":"10px","hamburgerBorderHover":"none","linkBoxShadowHover":"none","linkFontSize":"14px","backgroundOpacity":0.8,"controllerBorderRadius":"var(--lia-border-radius-50)","hamburgerBgColor":"transparent","hamburgerColor":"var(--lia-nav-controller-icon-color)","linkTextBorderBottom":"none","brandLogoHeight":"30px","linkBgHoverColor":"transparent","linkLetterSpacing":"normal","collapseMenuDividerOpacity":0.16,"dropdownPaddingBottom":"15px","paddingBottom":"15px","dropdownMenuOffset":"2px","hamburgerBgHoverColor":"transparent","borderBottom":"1px solid var(--lia-bs-border-color)","hamburgerBorder":"none","dropdownPaddingX":"10px","brandMarginRightSm":"10px","linkBoxShadow":"none","collapseMenuDividerBg":"var(--lia-nav-link-color)","linkColor":"var(--lia-bs-body-color)","linkJustifyContent":"flex-start","dropdownPaddingTop":"10px","controllerHighlightTextColor":"var(--lia-yiq-dark)","controllerTextColor":"var(--lia-nav-controller-icon-color)","background":{"imageAssetName":"","color":"var(--lia-bs-white)","size":"COVER","repeat":"NO_REPEAT","position":"CENTER_CENTER","imageLastModified":""},"linkBorderRadius":"var(--lia-bs-border-radius-sm)","linkHoverColor":"var(--lia-bs-body-color)","position":"FIXED","linkBorder":"none","linkTextBorderBottomHover":"2px solid var(--lia-bs-body-color)","brandMarginRight":"30px","hamburgerHoverColor":"var(--lia-nav-controller-icon-color)","linkBorderHover":"none","collapseMenuMarginLeft":"20px","linkFontStyle":"NORMAL","controllerTextHoverColor":"var(--lia-nav-controller-icon-hover-color)","linkPaddingX":"10px","linkPaddingY":"5px","paddingTop":"15px","linkTextTransform":"NONE","dropdownBorderColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.08)","controllerBgHoverColor":"hsla(var(--lia-bs-black-h), var(--lia-bs-black-s), var(--lia-bs-black-l), 0.1)","linkBgColor":"transparent","linkDropdownPaddingX":"var(--lia-nav-link-px)","linkDropdownPaddingY":"9px","controllerIconColor":"var(--lia-bs-body-color)","dropdownDividerMarginTop":"10px","linkGap":"10px","controllerIconHoverColor":"var(--lia-bs-body-color)"},"showSearchIcon":false,"languagePickerStyle":"iconAndLabel"},"__typename":"QuiltComponent"},{"id":"community.widget.breadcrumbWidget","props":{"backgroundColor":"transparent","linkHighlightColor":"var(--lia-bs-primary)","visualEffects":{"showBottomBorder":true},"linkTextColor":"var(--lia-bs-gray-700)"},"__typename":"QuiltComponent"},{"id":"custom.widget.community_banner","props":{"widgetVisibility":"signedInOrAnonymous","useTitle":true,"usePageWidth":false,"useBackground":false,"title":"","lazyLoad":false},"__typename":"QuiltComponent"},{"id":"custom.widget.HeroBanner","props":{"widgetVisibility":"signedInOrAnonymous","usePageWidth":false,"useTitle":true,"cMax_items":3,"useBackground":false,"title":"","lazyLoad":false,"widgetChooser":"custom.widget.HeroBanner"},"__typename":"QuiltComponent"}],"__typename":"QuiltWrapperSection"},"footer":{"backgroundImageProps":{"assetName":null,"backgroundSize":"COVER","backgroundRepeat":"NO_REPEAT","backgroundPosition":"CENTER_CENTER","lastModified":null,"__typename":"BackgroundImageProps"},"backgroundColor":"transparent","items":[{"id":"custom.widget.MicrosoftFooter","props":{"widgetVisibility":"signedInOrAnonymous","useTitle":true,"useBackground":false,"title":"","lazyLoad":false},"__typename":"QuiltComponent"}],"__typename":"QuiltWrapperSection"},"__typename":"QuiltWrapper","localOverride":false},"localOverride":false},"CachedAsset:text:en_US-components/common/ActionFeedback-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/common/ActionFeedback-1745505309750","value":{"joinedGroupHub.title":"Welcome","joinedGroupHub.message":"You are now a member of this group and are subscribed to updates.","groupHubInviteNotFound.title":"Invitation Not Found","groupHubInviteNotFound.message":"Sorry, we could not find your invitation to the group. The owner may have canceled the invite.","groupHubNotFound.title":"Group Not Found","groupHubNotFound.message":"The grouphub you tried to join does not exist. It may have been deleted.","existingGroupHubMember.title":"Already Joined","existingGroupHubMember.message":"You are already a member of this group.","accountLocked.title":"Account Locked","accountLocked.message":"Your account has been locked due to multiple failed attempts. Try again in {lockoutTime} minutes.","editedGroupHub.title":"Changes Saved","editedGroupHub.message":"Your group has been updated.","leftGroupHub.title":"Goodbye","leftGroupHub.message":"You are no longer a member of this group and will not receive future updates.","deletedGroupHub.title":"Deleted","deletedGroupHub.message":"The group has been deleted.","groupHubCreated.title":"Group Created","groupHubCreated.message":"{groupHubName} is ready to use","accountClosed.title":"Account Closed","accountClosed.message":"The account has been closed and you will now be redirected to the homepage","resetTokenExpired.title":"Reset Password Link has Expired","resetTokenExpired.message":"Try resetting your password again","invalidUrl.title":"Invalid URL","invalidUrl.message":"The URL you're using is not recognized. Verify your URL and try again.","accountClosedForUser.title":"Account Closed","accountClosedForUser.message":"{userName}'s account is closed","inviteTokenInvalid.title":"Invitation Invalid","inviteTokenInvalid.message":"Your invitation to the community has been canceled or expired.","inviteTokenError.title":"Invitation Verification Failed","inviteTokenError.message":"The url you are utilizing is not recognized. Verify your URL and try again","pageNotFound.title":"Access Denied","pageNotFound.message":"You do not have access to this area of the community or it doesn't exist","eventAttending.title":"Responded as Attending","eventAttending.message":"You'll be notified when there's new activity and reminded as the event approaches","eventInterested.title":"Responded as Interested","eventInterested.message":"You'll be notified when there's new activity and reminded as the event approaches","eventNotFound.title":"Event Not Found","eventNotFound.message":"The event you tried to respond to does not exist.","redirectToRelatedPage.title":"Showing Related Content","redirectToRelatedPageForBaseUsers.title":"Showing Related Content","redirectToRelatedPageForBaseUsers.message":"The content you are trying to access is archived","redirectToRelatedPage.message":"The content you are trying to access is archived","relatedUrl.archivalLink.flyoutMessage":"The content you are trying to access is archived View Archived Content"},"localOverride":false},"CachedAsset:component:custom.widget.community_banner-en-1744400827890":{"__typename":"CachedAsset","id":"component:custom.widget.community_banner-en-1744400827890","value":{"component":{"id":"custom.widget.community_banner","template":{"id":"community_banner","markupLanguage":"HANDLEBARS","style":".community-banner {\n a.top-bar.btn {\n top: 0px;\n width: 100%;\n z-index: 999;\n text-align: center;\n left: 0px;\n background: #0068b8;\n color: white;\n padding: 10px 0px;\n display: block;\n box-shadow: none !important;\n border: none !important;\n border-radius: none !important;\n margin: 0px !important;\n font-size: 14px;\n }\n}\n","texts":null,"defaults":{"config":{"applicablePages":[],"description":"community announcement text","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.community_banner","form":null,"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":"community announcement text","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"form":null,"__typename":"Component","localOverride":false},"globalCss":{"css":".custom_widget_community_banner_community-banner_1x9u2_1 {\n a.custom_widget_community_banner_top-bar_1x9u2_2.custom_widget_community_banner_btn_1x9u2_2 {\n top: 0;\n width: 100%;\n z-index: 999;\n text-align: center;\n left: 0;\n background: #0068b8;\n color: white;\n padding: 0.625rem 0;\n display: block;\n box-shadow: none !important;\n border: none !important;\n border-radius: none !important;\n margin: 0 !important;\n font-size: 0.875rem;\n }\n}\n","tokens":{"community-banner":"custom_widget_community_banner_community-banner_1x9u2_1","top-bar":"custom_widget_community_banner_top-bar_1x9u2_2","btn":"custom_widget_community_banner_btn_1x9u2_2"}},"form":null},"localOverride":false},"CachedAsset:component:custom.widget.HeroBanner-en-1744400827890":{"__typename":"CachedAsset","id":"component:custom.widget.HeroBanner-en-1744400827890","value":{"component":{"id":"custom.widget.HeroBanner","template":{"id":"HeroBanner","markupLanguage":"REACT","style":null,"texts":{"searchPlaceholderText":"Search this community","followActionText":"Follow","unfollowActionText":"Following","searchOnHoverText":"Please enter your search term(s) and then press return key to complete a search.","blogs.sidebar.pagetitle":"Latest Blogs | Microsoft Tech Community","followThisNode":"Follow this node","unfollowThisNode":"Unfollow this node"},"defaults":{"config":{"applicablePages":[],"description":null,"fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[{"id":"max_items","dataType":"NUMBER","list":false,"defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"control":"INPUT","__typename":"PropDefinition"}],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.HeroBanner","form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"},"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":null,"fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[{"id":"max_items","dataType":"NUMBER","list":false,"defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"control":"INPUT","__typename":"PropDefinition"}],"__typename":"ComponentProperties"},"form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"},"__typename":"Component","localOverride":false},"globalCss":null,"form":{"fields":[{"id":"widgetChooser","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"title","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useTitle","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"useBackground","validation":null,"noValidation":null,"dataType":"BOOLEAN","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"widgetVisibility","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"moreOptions","validation":null,"noValidation":null,"dataType":"STRING","list":null,"control":null,"defaultValue":null,"label":null,"description":null,"possibleValues":null,"__typename":"FormField"},{"id":"cMax_items","validation":null,"noValidation":null,"dataType":"NUMBER","list":false,"control":"INPUT","defaultValue":"3","label":"Max Items","description":"The maximum number of items to display in the carousel","possibleValues":null,"__typename":"FormField"}],"layout":{"rows":[{"id":"widgetChooserGroup","type":"fieldset","as":null,"items":[{"id":"widgetChooser","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"titleGroup","type":"fieldset","as":null,"items":[{"id":"title","className":null,"__typename":"FormFieldRef"},{"id":"useTitle","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"useBackground","type":"fieldset","as":null,"items":[{"id":"useBackground","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"widgetVisibility","type":"fieldset","as":null,"items":[{"id":"widgetVisibility","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"moreOptionsGroup","type":"fieldset","as":null,"items":[{"id":"moreOptions","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"},{"id":"componentPropsGroup","type":"fieldset","as":null,"items":[{"id":"cMax_items","className":null,"__typename":"FormFieldRef"}],"props":null,"legend":null,"description":null,"className":null,"viewVariant":null,"toggleState":null,"__typename":"FormFieldset"}],"actionButtons":null,"className":"custom_widget_HeroBanner_form","formGroupFieldSeparator":"divider","__typename":"FormLayout"},"__typename":"Form"}},"localOverride":false},"CachedAsset:component:custom.widget.MicrosoftFooter-en-1744400827890":{"__typename":"CachedAsset","id":"component:custom.widget.MicrosoftFooter-en-1744400827890","value":{"component":{"id":"custom.widget.MicrosoftFooter","template":{"id":"MicrosoftFooter","markupLanguage":"HANDLEBARS","style":".context-uhf {\n min-width: 280px;\n font-size: 15px;\n box-sizing: border-box;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n & *,\n & *:before,\n & *:after {\n box-sizing: inherit;\n }\n a.c-uhff-link {\n color: #616161;\n word-break: break-word;\n text-decoration: none;\n }\n &a:link,\n &a:focus,\n &a:hover,\n &a:active,\n &a:visited {\n text-decoration: none;\n color: inherit;\n }\n & div {\n font-family: 'Segoe UI', SegoeUI, 'Helvetica Neue', Helvetica, Arial, sans-serif;\n }\n}\n.c-uhff {\n background: #f2f2f2;\n margin: -1.5625;\n width: auto;\n height: auto;\n}\n.c-uhff-nav {\n margin: 0 auto;\n max-width: calc(1600px + 10%);\n padding: 0 5%;\n box-sizing: inherit;\n &:before,\n &:after {\n content: ' ';\n display: table;\n clear: left;\n }\n @media only screen and (max-width: 1083px) {\n padding-left: 12px;\n }\n .c-heading-4 {\n color: #616161;\n word-break: break-word;\n font-size: 15px;\n line-height: 20px;\n padding: 36px 0 4px;\n font-weight: 600;\n }\n .c-uhff-nav-row {\n .c-uhff-nav-group {\n display: block;\n float: left;\n min-height: 1px;\n vertical-align: text-top;\n padding: 0 12px;\n width: 100%;\n zoom: 1;\n &:first-child {\n padding-left: 0;\n @media only screen and (max-width: 1083px) {\n padding-left: 12px;\n }\n }\n @media only screen and (min-width: 540px) and (max-width: 1082px) {\n width: 33.33333%;\n }\n @media only screen and (min-width: 1083px) {\n width: 16.6666666667%;\n }\n ul.c-list.f-bare {\n font-size: 11px;\n line-height: 16px;\n margin-top: 0;\n margin-bottom: 0;\n padding-left: 0;\n list-style-type: none;\n li {\n word-break: break-word;\n padding: 8px 0;\n margin: 0;\n }\n }\n }\n }\n}\n.c-uhff-base {\n background: #f2f2f2;\n margin: 0 auto;\n max-width: calc(1600px + 10%);\n padding: 30px 5% 16px;\n &:before,\n &:after {\n content: ' ';\n display: table;\n }\n &:after {\n clear: both;\n }\n a.c-uhff-ccpa {\n font-size: 11px;\n line-height: 16px;\n float: left;\n margin: 3px 0;\n }\n a.c-uhff-ccpa:hover {\n text-decoration: underline;\n }\n ul.c-list {\n font-size: 11px;\n line-height: 16px;\n float: right;\n margin: 3px 0;\n color: #616161;\n li {\n padding: 0 24px 4px 0;\n display: inline-block;\n }\n }\n .c-list.f-bare {\n padding-left: 0;\n list-style-type: none;\n }\n @media only screen and (max-width: 1083px) {\n display: flex;\n flex-wrap: wrap;\n padding: 30px 24px 16px;\n }\n}\n\n.social-share {\n position: fixed;\n top: 60%;\n transform: translateY(-50%);\n left: 0;\n z-index: 1000;\n}\n\n.sharing-options {\n list-style: none;\n padding: 0;\n margin: 0;\n display: block;\n flex-direction: column;\n background-color: white;\n width: 43px;\n border-radius: 0px 7px 7px 0px;\n}\n.linkedin-icon {\n border-top-right-radius: 7px;\n}\n.linkedin-icon:hover {\n border-radius: 0;\n}\n.social-share-rss-image {\n border-bottom-right-radius: 7px;\n}\n.social-share-rss-image:hover {\n border-radius: 0;\n}\n\n.social-link-footer {\n position: relative;\n display: block;\n margin: -2px 0;\n transition: all 0.2s ease;\n}\n.social-link-footer:hover .linkedin-icon {\n border-radius: 0;\n}\n.social-link-footer:hover .social-share-rss-image {\n border-radius: 0;\n}\n\n.social-link-footer img {\n width: 40px;\n height: auto;\n transition: filter 0.3s ease;\n}\n\n.social-share-list {\n width: 40px;\n}\n.social-share-rss-image {\n width: 40px;\n}\n\n.share-icon {\n border: 2px solid transparent;\n display: inline-block;\n position: relative;\n}\n\n.share-icon:hover {\n opacity: 1;\n border: 2px solid white;\n box-sizing: border-box;\n}\n\n.share-icon:hover .label {\n opacity: 1;\n visibility: visible;\n border: 2px solid white;\n box-sizing: border-box;\n border-left: none;\n}\n\n.label {\n position: absolute;\n left: 100%;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 0.2s ease;\n color: white;\n border-radius: 0 10 0 10px;\n top: 50%;\n transform: translateY(-50%);\n height: 40px;\n border-radius: 0 6px 6px 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px 5px 20px 8px;\n margin-left: -1px;\n}\n.linkedin {\n background-color: #0474b4;\n}\n.facebook {\n background-color: #3c5c9c;\n}\n.twitter {\n background-color: white;\n color: black;\n}\n.reddit {\n background-color: #fc4404;\n}\n.mail {\n background-color: #848484;\n}\n.bluesky {\n background-color: white;\n color: black;\n}\n.rss {\n background-color: #ec7b1c;\n}\n#RSS {\n width: 40px;\n height: 40px;\n}\n\n@media (max-width: 991px) {\n .social-share {\n display: none;\n }\n}\n","texts":{"New tab":"What's New","New 1":"Surface Laptop Studio 2","New 2":"Surface Laptop Go 3","New 3":"Surface Pro 9","New 4":"Surface Laptop 5","New 5":"Surface Studio 2+","New 6":"Copilot in Windows","New 7":"Microsoft 365","New 8":"Windows 11 apps","Store tab":"Microsoft Store","Store 1":"Account Profile","Store 2":"Download Center","Store 3":"Microsoft Store Support","Store 4":"Returns","Store 5":"Order tracking","Store 6":"Certified Refurbished","Store 7":"Microsoft Store Promise","Store 8":"Flexible Payments","Education tab":"Education","Edu 1":"Microsoft in education","Edu 2":"Devices for education","Edu 3":"Microsoft Teams for Education","Edu 4":"Microsoft 365 Education","Edu 5":"How to buy for your school","Edu 6":"Educator Training and development","Edu 7":"Deals for students and parents","Edu 8":"Azure for students","Business tab":"Business","Bus 1":"Microsoft Cloud","Bus 2":"Microsoft Security","Bus 3":"Dynamics 365","Bus 4":"Microsoft 365","Bus 5":"Microsoft Power Platform","Bus 6":"Microsoft Teams","Bus 7":"Microsoft Industry","Bus 8":"Small Business","Developer tab":"Developer & IT","Dev 1":"Azure","Dev 2":"Developer Center","Dev 3":"Documentation","Dev 4":"Microsoft Learn","Dev 5":"Microsoft Tech Community","Dev 6":"Azure Marketplace","Dev 7":"AppSource","Dev 8":"Visual Studio","Company tab":"Company","Com 1":"Careers","Com 2":"About Microsoft","Com 3":"Company News","Com 4":"Privacy at Microsoft","Com 5":"Investors","Com 6":"Diversity and inclusion","Com 7":"Accessiblity","Com 8":"Sustainibility"},"defaults":{"config":{"applicablePages":[],"description":"The Microsoft Footer","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"components":[{"id":"custom.widget.MicrosoftFooter","form":null,"config":null,"props":[],"__typename":"Component"}],"grouping":"CUSTOM","__typename":"ComponentTemplate"},"properties":{"config":{"applicablePages":[],"description":"The Microsoft Footer","fetchedContent":null,"__typename":"ComponentConfiguration"},"props":[],"__typename":"ComponentProperties"},"form":null,"__typename":"Component","localOverride":false},"globalCss":{"css":".custom_widget_MicrosoftFooter_context-uhf_105bp_1 {\n min-width: 17.5rem;\n font-size: 0.9375rem;\n box-sizing: border-box;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n & *,\n & *:before,\n & *:after {\n box-sizing: inherit;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-link_105bp_12 {\n color: #616161;\n word-break: break-word;\n text-decoration: none;\n }\n &a:link,\n &a:focus,\n &a:hover,\n &a:active,\n &a:visited {\n text-decoration: none;\n color: inherit;\n }\n & div {\n font-family: 'Segoe UI', SegoeUI, 'Helvetica Neue', Helvetica, Arial, sans-serif;\n }\n}\n.custom_widget_MicrosoftFooter_c-uhff_105bp_12 {\n background: #f2f2f2;\n margin: -1.5625;\n width: auto;\n height: auto;\n}\n.custom_widget_MicrosoftFooter_c-uhff-nav_105bp_35 {\n margin: 0 auto;\n max-width: calc(100rem + 10%);\n padding: 0 5%;\n box-sizing: inherit;\n &:before,\n &:after {\n content: ' ';\n display: table;\n clear: left;\n }\n @media only screen and (max-width: 1083px) {\n padding-left: 0.75rem;\n }\n .custom_widget_MicrosoftFooter_c-heading-4_105bp_49 {\n color: #616161;\n word-break: break-word;\n font-size: 0.9375rem;\n line-height: 1.25rem;\n padding: 2.25rem 0 0.25rem;\n font-weight: 600;\n }\n .custom_widget_MicrosoftFooter_c-uhff-nav-row_105bp_57 {\n .custom_widget_MicrosoftFooter_c-uhff-nav-group_105bp_58 {\n display: block;\n float: left;\n min-height: 0.0625rem;\n vertical-align: text-top;\n padding: 0 0.75rem;\n width: 100%;\n zoom: 1;\n &:first-child {\n padding-left: 0;\n @media only screen and (max-width: 1083px) {\n padding-left: 0.75rem;\n }\n }\n @media only screen and (min-width: 540px) and (max-width: 1082px) {\n width: 33.33333%;\n }\n @media only screen and (min-width: 1083px) {\n width: 16.6666666667%;\n }\n ul.custom_widget_MicrosoftFooter_c-list_105bp_78.custom_widget_MicrosoftFooter_f-bare_105bp_78 {\n font-size: 0.6875rem;\n line-height: 1rem;\n margin-top: 0;\n margin-bottom: 0;\n padding-left: 0;\n list-style-type: none;\n li {\n word-break: break-word;\n padding: 0.5rem 0;\n margin: 0;\n }\n }\n }\n }\n}\n.custom_widget_MicrosoftFooter_c-uhff-base_105bp_94 {\n background: #f2f2f2;\n margin: 0 auto;\n max-width: calc(100rem + 10%);\n padding: 1.875rem 5% 1rem;\n &:before,\n &:after {\n content: ' ';\n display: table;\n }\n &:after {\n clear: both;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107 {\n font-size: 0.6875rem;\n line-height: 1rem;\n float: left;\n margin: 0.1875rem 0;\n }\n a.custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107:hover {\n text-decoration: underline;\n }\n ul.custom_widget_MicrosoftFooter_c-list_105bp_78 {\n font-size: 0.6875rem;\n line-height: 1rem;\n float: right;\n margin: 0.1875rem 0;\n color: #616161;\n li {\n padding: 0 1.5rem 0.25rem 0;\n display: inline-block;\n }\n }\n .custom_widget_MicrosoftFooter_c-list_105bp_78.custom_widget_MicrosoftFooter_f-bare_105bp_78 {\n padding-left: 0;\n list-style-type: none;\n }\n @media only screen and (max-width: 1083px) {\n display: flex;\n flex-wrap: wrap;\n padding: 1.875rem 1.5rem 1rem;\n }\n}\n.custom_widget_MicrosoftFooter_social-share_105bp_138 {\n position: fixed;\n top: 60%;\n transform: translateY(-50%);\n left: 0;\n z-index: 1000;\n}\n.custom_widget_MicrosoftFooter_sharing-options_105bp_146 {\n list-style: none;\n padding: 0;\n margin: 0;\n display: block;\n flex-direction: column;\n background-color: white;\n width: 2.6875rem;\n border-radius: 0 0.4375rem 0.4375rem 0;\n}\n.custom_widget_MicrosoftFooter_linkedin-icon_105bp_156 {\n border-top-right-radius: 7px;\n}\n.custom_widget_MicrosoftFooter_linkedin-icon_105bp_156:hover {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n border-bottom-right-radius: 7px;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162:hover {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169 {\n position: relative;\n display: block;\n margin: -0.125rem 0;\n transition: all 0.2s ease;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169:hover .custom_widget_MicrosoftFooter_linkedin-icon_105bp_156 {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169:hover .custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n border-radius: 0;\n}\n.custom_widget_MicrosoftFooter_social-link-footer_105bp_169 img {\n width: 2.5rem;\n height: auto;\n transition: filter 0.3s ease;\n}\n.custom_widget_MicrosoftFooter_social-share-list_105bp_188 {\n width: 2.5rem;\n}\n.custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162 {\n width: 2.5rem;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195 {\n border: 2px solid transparent;\n display: inline-block;\n position: relative;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195:hover {\n opacity: 1;\n border: 2px solid white;\n box-sizing: border-box;\n}\n.custom_widget_MicrosoftFooter_share-icon_105bp_195:hover .custom_widget_MicrosoftFooter_label_105bp_207 {\n opacity: 1;\n visibility: visible;\n border: 2px solid white;\n box-sizing: border-box;\n border-left: none;\n}\n.custom_widget_MicrosoftFooter_label_105bp_207 {\n position: absolute;\n left: 100%;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 0.2s ease;\n color: white;\n border-radius: 0 10 0 0.625rem;\n top: 50%;\n transform: translateY(-50%);\n height: 2.5rem;\n border-radius: 0 0.375rem 0.375rem 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.25rem 0.3125rem 1.25rem 0.5rem;\n margin-left: -0.0625rem;\n}\n.custom_widget_MicrosoftFooter_linkedin_105bp_156 {\n background-color: #0474b4;\n}\n.custom_widget_MicrosoftFooter_facebook_105bp_237 {\n background-color: #3c5c9c;\n}\n.custom_widget_MicrosoftFooter_twitter_105bp_240 {\n background-color: white;\n color: black;\n}\n.custom_widget_MicrosoftFooter_reddit_105bp_244 {\n background-color: #fc4404;\n}\n.custom_widget_MicrosoftFooter_mail_105bp_247 {\n background-color: #848484;\n}\n.custom_widget_MicrosoftFooter_bluesky_105bp_250 {\n background-color: white;\n color: black;\n}\n.custom_widget_MicrosoftFooter_rss_105bp_254 {\n background-color: #ec7b1c;\n}\n#custom_widget_MicrosoftFooter_RSS_105bp_1 {\n width: 2.5rem;\n height: 2.5rem;\n}\n@media (max-width: 991px) {\n .custom_widget_MicrosoftFooter_social-share_105bp_138 {\n display: none;\n }\n}\n","tokens":{"context-uhf":"custom_widget_MicrosoftFooter_context-uhf_105bp_1","c-uhff-link":"custom_widget_MicrosoftFooter_c-uhff-link_105bp_12","c-uhff":"custom_widget_MicrosoftFooter_c-uhff_105bp_12","c-uhff-nav":"custom_widget_MicrosoftFooter_c-uhff-nav_105bp_35","c-heading-4":"custom_widget_MicrosoftFooter_c-heading-4_105bp_49","c-uhff-nav-row":"custom_widget_MicrosoftFooter_c-uhff-nav-row_105bp_57","c-uhff-nav-group":"custom_widget_MicrosoftFooter_c-uhff-nav-group_105bp_58","c-list":"custom_widget_MicrosoftFooter_c-list_105bp_78","f-bare":"custom_widget_MicrosoftFooter_f-bare_105bp_78","c-uhff-base":"custom_widget_MicrosoftFooter_c-uhff-base_105bp_94","c-uhff-ccpa":"custom_widget_MicrosoftFooter_c-uhff-ccpa_105bp_107","social-share":"custom_widget_MicrosoftFooter_social-share_105bp_138","sharing-options":"custom_widget_MicrosoftFooter_sharing-options_105bp_146","linkedin-icon":"custom_widget_MicrosoftFooter_linkedin-icon_105bp_156","social-share-rss-image":"custom_widget_MicrosoftFooter_social-share-rss-image_105bp_162","social-link-footer":"custom_widget_MicrosoftFooter_social-link-footer_105bp_169","social-share-list":"custom_widget_MicrosoftFooter_social-share-list_105bp_188","share-icon":"custom_widget_MicrosoftFooter_share-icon_105bp_195","label":"custom_widget_MicrosoftFooter_label_105bp_207","linkedin":"custom_widget_MicrosoftFooter_linkedin_105bp_156","facebook":"custom_widget_MicrosoftFooter_facebook_105bp_237","twitter":"custom_widget_MicrosoftFooter_twitter_105bp_240","reddit":"custom_widget_MicrosoftFooter_reddit_105bp_244","mail":"custom_widget_MicrosoftFooter_mail_105bp_247","bluesky":"custom_widget_MicrosoftFooter_bluesky_105bp_250","rss":"custom_widget_MicrosoftFooter_rss_105bp_254","RSS":"custom_widget_MicrosoftFooter_RSS_105bp_1"}},"form":null},"localOverride":false},"CachedAsset:text:en_US-components/community/Breadcrumb-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/community/Breadcrumb-1745505309750","value":{"navLabel":"Breadcrumbs","dropdown":"Additional parent page navigation"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageBanner-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBanner-1745505309750","value":{"messageMarkedAsSpam":"This post has been marked as spam","messageMarkedAsSpam@board:TKB":"This article has been marked as spam","messageMarkedAsSpam@board:BLOG":"This post has been marked as spam","messageMarkedAsSpam@board:FORUM":"This discussion has been marked as spam","messageMarkedAsSpam@board:OCCASION":"This event has been marked as spam","messageMarkedAsSpam@board:IDEA":"This idea has been marked as spam","manageSpam":"Manage Spam","messageMarkedAsAbuse":"This post has been marked as abuse","messageMarkedAsAbuse@board:TKB":"This article has been marked as abuse","messageMarkedAsAbuse@board:BLOG":"This post has been marked as abuse","messageMarkedAsAbuse@board:FORUM":"This discussion has been marked as abuse","messageMarkedAsAbuse@board:OCCASION":"This event has been marked as abuse","messageMarkedAsAbuse@board:IDEA":"This idea has been marked as abuse","preModCommentAuthorText":"This comment will be published as soon as it is approved","preModCommentModeratorText":"This comment is awaiting moderation","messageMarkedAsOther":"This post has been rejected due to other reasons","messageMarkedAsOther@board:TKB":"This article has been rejected due to other reasons","messageMarkedAsOther@board:BLOG":"This post has been rejected due to other reasons","messageMarkedAsOther@board:FORUM":"This discussion has been rejected due to other reasons","messageMarkedAsOther@board:OCCASION":"This event has been rejected due to other reasons","messageMarkedAsOther@board:IDEA":"This idea has been rejected due to other reasons","messageArchived":"This post was archived on {date}","relatedUrl":"View Related Content","relatedContentText":"Showing related content","archivedContentLink":"View Archived Content"},"localOverride":false},"Category:category:Exchange":{"__typename":"Category","id":"category:Exchange","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Outlook":{"__typename":"Category","id":"category:Outlook","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Community-Info-Center":{"__typename":"Category","id":"category:Community-Info-Center","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:DrivingAdoption":{"__typename":"Category","id":"category:DrivingAdoption","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Azure":{"__typename":"Category","id":"category:Azure","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Windows-Server":{"__typename":"Category","id":"category:Windows-Server","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftTeams":{"__typename":"Category","id":"category:MicrosoftTeams","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:PublicSector":{"__typename":"Category","id":"category:PublicSector","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoft365":{"__typename":"Category","id":"category:microsoft365","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:IoT":{"__typename":"Category","id":"category:IoT","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:HealthcareAndLifeSciences":{"__typename":"Category","id":"category:HealthcareAndLifeSciences","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:ITOpsTalk":{"__typename":"Category","id":"category:ITOpsTalk","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftLearn":{"__typename":"Category","id":"category:MicrosoftLearn","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Blog:board:MicrosoftLearnBlog":{"__typename":"Blog","id":"board:MicrosoftLearnBlog","blogPolicies":{"__typename":"BlogPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}},"boardPolicies":{"__typename":"BoardPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:AI":{"__typename":"Category","id":"category:AI","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftMechanics":{"__typename":"Category","id":"category:MicrosoftMechanics","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:MicrosoftforNonprofits":{"__typename":"Category","id":"category:MicrosoftforNonprofits","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:StartupsatMicrosoft":{"__typename":"Category","id":"category:StartupsatMicrosoft","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:PartnerCommunity":{"__typename":"Category","id":"category:PartnerCommunity","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Microsoft365Copilot":{"__typename":"Category","id":"category:Microsoft365Copilot","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Windows":{"__typename":"Category","id":"category:Windows","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:Content_Management":{"__typename":"Category","id":"category:Content_Management","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoft-security":{"__typename":"Category","id":"category:microsoft-security","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"Category:category:microsoftintune":{"__typename":"Category","id":"category:microsoftintune","categoryPolicies":{"__typename":"CategoryPolicies","canReadNode":{"__typename":"PolicyResult","failureReason":null}}},"QueryVariables:TopicReplyList:message:381053:2":{"__typename":"QueryVariables","id":"TopicReplyList:message:381053:2","value":{"id":"message:381053","first":10,"sorts":{"postTime":{"direction":"DESC"}},"repliesFirst":3,"repliesFirstDepthThree":1,"repliesSorts":{"postTime":{"direction":"DESC"}},"useAvatar":true,"useAuthorLogin":true,"useAuthorRank":true,"useBody":true,"useKudosCount":true,"useTimeToRead":false,"useMedia":false,"useReadOnlyIcon":false,"useRepliesCount":true,"useSearchSnippet":false,"useAcceptedSolutionButton":false,"useSolvedBadge":false,"useAttachments":false,"attachmentsFirst":5,"useTags":true,"useNodeAncestors":false,"useUserHoverCard":false,"useNodeHoverCard":false,"useModerationStatus":true,"usePreviewSubjectModal":false,"useMessageStatus":true}},"ROOT_MUTATION":{"__typename":"Mutation"},"CachedAsset:text:en_US-components/community/Navbar-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/community/Navbar-1745505309750","value":{"community":"Community Home","inbox":"Inbox","manageContent":"Manage Content","tos":"Terms of Service","forgotPassword":"Forgot Password","themeEditor":"Theme Editor","edit":"Edit Navigation Bar","skipContent":"Skip to content","gxcuf89792":"Tech Community","external-1":"Events","s-m-b":"Nonprofit Community","windows-server":"Windows Server","education-sector":"Education Sector","driving-adoption":"Driving Adoption","Common-content_management-link":"Content Management","microsoft-learn":"Microsoft Learn","s-q-l-server":"Content Management","partner-community":"Microsoft Partner Community","microsoft365":"Microsoft 365","external-9":".NET","external-8":"Teams","external-7":"Github","products-services":"Products","external-6":"Power Platform","communities-1":"Topics","external-5":"Microsoft Security","planner":"Outlook","external-4":"Microsoft 365","external-3":"Dynamics 365","azure":"Azure","healthcare-and-life-sciences":"Healthcare and Life Sciences","external-2":"Azure","microsoft-mechanics":"Microsoft Mechanics","microsoft-learn-1":"Community","external-10":"Learning Room Directory","microsoft-learn-blog":"Blog","windows":"Windows","i-t-ops-talk":"ITOps Talk","external-link-1":"View All","microsoft-securityand-compliance":"Microsoft Security","public-sector":"Public Sector","community-info-center":"Lounge","external-link-2":"View All","microsoft-teams":"Microsoft Teams","external":"Blogs","microsoft-endpoint-manager":"Microsoft Intune","startupsat-microsoft":"Startups at Microsoft","exchange":"Exchange","a-i":"AI and Machine Learning","io-t":"Internet of Things (IoT)","Common-microsoft365-copilot-link":"Microsoft 365 Copilot","outlook":"Microsoft 365 Copilot","external-link":"Community Hubs","communities":"Products"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarHamburgerDropdown-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarHamburgerDropdown-1745505309750","value":{"hamburgerLabel":"Side Menu"},"localOverride":false},"CachedAsset:text:en_US-components/community/BrandLogo-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/community/BrandLogo-1745505309750","value":{"logoAlt":"Khoros","themeLogoAlt":"Brand Logo"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarTextLinks-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarTextLinks-1745505309750","value":{"more":"More"},"localOverride":false},"CachedAsset:text:en_US-components/authentication/AuthenticationLink-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/authentication/AuthenticationLink-1745505309750","value":{"title.login":"Sign In","title.registration":"Register","title.forgotPassword":"Forgot Password","title.multiAuthLogin":"Sign In"},"localOverride":false},"CachedAsset:text:en_US-components/nodes/NodeLink-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/nodes/NodeLink-1745505309750","value":{"place":"Place {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageView/MessageViewStandard-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageView/MessageViewStandard-1745505309750","value":{"anonymous":"Anonymous","author":"{messageAuthorLogin}","authorBy":"{messageAuthorLogin}","board":"{messageBoardTitle}","replyToUser":" to {parentAuthor}","showMoreReplies":"Show More","replyText":"Reply","repliesText":"Replies","markedAsSolved":"Marked as Solved","movedMessagePlaceholder.BLOG":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.TKB":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.FORUM":"{count, plural, =0 {This reply has been} other {These replies have been} }","movedMessagePlaceholder.IDEA":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholder.OCCASION":"{count, plural, =0 {This comment has been} other {These comments have been} }","movedMessagePlaceholderUrlText":"moved.","messageStatus":"Status: ","statusChanged":"Status changed: {previousStatus} to {currentStatus}","statusAdded":"Status added: {status}","statusRemoved":"Status removed: {status}","labelExpand":"expand replies","labelCollapse":"collapse replies","unhelpfulReason.reason1":"Content is outdated","unhelpfulReason.reason2":"Article is missing information","unhelpfulReason.reason3":"Content is for a different Product","unhelpfulReason.reason4":"Doesn't match what I was searching for"},"localOverride":false},"CachedAsset:text:en_US-components/messages/ThreadedReplyList-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/ThreadedReplyList-1745505309750","value":{"title":"{count, plural, one{# Reply} other{# Replies}}","title@board:BLOG":"{count, plural, one{# Comment} other{# Comments}}","title@board:TKB":"{count, plural, one{# Comment} other{# Comments}}","title@board:IDEA":"{count, plural, one{# Comment} other{# Comments}}","title@board:OCCASION":"{count, plural, one{# Comment} other{# Comments}}","noRepliesTitle":"No Replies","noRepliesTitle@board:BLOG":"No Comments","noRepliesTitle@board:TKB":"No Comments","noRepliesTitle@board:IDEA":"No Comments","noRepliesTitle@board:OCCASION":"No Comments","noRepliesDescription":"Be the first to reply","noRepliesDescription@board:BLOG":"Be the first to comment","noRepliesDescription@board:TKB":"Be the first to comment","noRepliesDescription@board:IDEA":"Be the first to comment","noRepliesDescription@board:OCCASION":"Be the first to comment","messageReadOnlyAlert:BLOG":"Comments have been turned off for this post","messageReadOnlyAlert:TKB":"Comments have been turned off for this article","messageReadOnlyAlert:IDEA":"Comments have been turned off for this idea","messageReadOnlyAlert:FORUM":"Replies have been turned off for this discussion","messageReadOnlyAlert:OCCASION":"Comments have been turned off for this event"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageReplyCallToAction-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyCallToAction-1745505309750","value":{"leaveReply":"Leave a reply...","leaveReply@board:BLOG@message:root":"Leave a comment...","leaveReply@board:TKB@message:root":"Leave a comment...","leaveReply@board:IDEA@message:root":"Leave a comment...","leaveReply@board:OCCASION@message:root":"Leave a comment...","repliesTurnedOff.FORUM":"Replies are turned off for this topic","repliesTurnedOff.BLOG":"Comments are turned off for this topic","repliesTurnedOff.TKB":"Comments are turned off for this topic","repliesTurnedOff.IDEA":"Comments are turned off for this topic","repliesTurnedOff.OCCASION":"Comments are turned off for this topic","infoText":"Stop poking me!"},"localOverride":false},"CachedAsset:text:en_US-components/community/NavbarDropdownToggle-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/community/NavbarDropdownToggle-1745505309750","value":{"ariaLabelClosed":"Press the down arrow to open the menu"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/common/QueryHandler-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/common/QueryHandler-1745505309750","value":{"title":"Query Handler"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageCoverImage-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCoverImage-1745505309750","value":{"coverImageTitle":"Cover Image"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeTitle-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeTitle-1745505309750","value":{"nodeTitle":"{nodeTitle, select, community {Community} other {{nodeTitle}}} "},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTimeToRead-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTimeToRead-1745505309750","value":{"minReadText":"{min} MIN READ"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageSubject-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageSubject-1745505309750","value":{"noSubject":"(no subject)"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserLink-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserLink-1745505309750","value":{"authorName":"View Profile: {author}","anonymous":"Anonymous"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserRank-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserRank-1745505309750","value":{"rankName":"{rankName}","userRank":"Author rank {rankName}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageTime-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageTime-1745505309750","value":{"postTime":"Published: {time}","lastPublishTime":"Last Update: {time}","conversation.lastPostingActivityTime":"Last posting activity time: {time}","conversation.lastPostTime":"Last post time: {time}","moderationData.rejectTime":"Rejected time: {time}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageBody-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageBody-1745505309750","value":{"showMessageBody":"Show More","mentionsErrorTitle":"{mentionsType, select, board {Board} user {User} message {Message} other {}} No Longer Available","mentionsErrorMessage":"The {mentionsType} you are trying to view has been removed from the community.","videoProcessing":"Video is being processed. Please try again in a few minutes.","bannerTitle":"Video provider requires cookies to play the video. Accept to continue or {url} it directly on the provider's site.","buttonTitle":"Accept","urlText":"watch"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageCustomFields-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageCustomFields-1745505309750","value":{"CustomField.default.label":"Value of {name}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageRevision-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageRevision-1745505309750","value":{"lastUpdatedDatePublished":"{publishCount, plural, one{Published} other{Updated}} {date}","lastUpdatedDateDraft":"Created {date}","version":"Version {major}.{minor}"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageReplyButton-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageReplyButton-1745505309750","value":{"repliesCount":"{count}","title":"Reply","title@board:BLOG@message:root":"Comment","title@board:TKB@message:root":"Comment","title@board:IDEA@message:root":"Comment","title@board:OCCASION@message:root":"Comment"},"localOverride":false},"CachedAsset:text:en_US-components/messages/MessageAuthorBio-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/messages/MessageAuthorBio-1745505309750","value":{"sendMessage":"Send Message","actionMessage":"Follow this blog board to get notified when there's new activity","coAuthor":"CO-PUBLISHER","contributor":"CONTRIBUTOR","userProfile":"View Profile","iconlink":"Go to {name} {type}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/users/UserAvatar-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/users/UserAvatar-1745505309750","value":{"altText":"{login}'s avatar","altTextGeneric":"User's avatar"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/ranks/UserRankLabel-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/ranks/UserRankLabel-1745505309750","value":{"altTitle":"Icon for {rankName} rank"},"localOverride":false},"CachedAsset:text:en_US-components/users/UserRegistrationDate-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/users/UserRegistrationDate-1745505309750","value":{"noPrefix":"{date}","withPrefix":"Joined {date}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeAvatar-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeAvatar-1745505309750","value":{"altTitle":"Node avatar for {nodeTitle}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeDescription-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeDescription-1745505309750","value":{"description":"{description}"},"localOverride":false},"CachedAsset:text:en_US-components/tags/TagView/TagViewChip-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-components/tags/TagView/TagViewChip-1745505309750","value":{"tagLabelName":"Tag name {tagName}"},"localOverride":false},"CachedAsset:text:en_US-shared/client/components/nodes/NodeIcon-1745505309750":{"__typename":"CachedAsset","id":"text:en_US-shared/client/components/nodes/NodeIcon-1745505309750","value":{"contentType":"Content Type {style, select, FORUM {Forum} BLOG {Blog} TKB {Knowledge Base} IDEA {Ideas} OCCASION {Events} other {}} icon"},"localOverride":false}}}},"page":"/blogs/BlogMessagePage/BlogMessagePage","query":{"boardId":"educatordeveloperblog","messageSubject":"entity-component-system-in-unity-8211-a-tutorial","messageId":"381053"},"buildId":"HEhyUrv5OXNBIbfCLaOrw","runtimeConfig":{"buildInformationVisible":false,"logLevelApp":"info","logLevelMetrics":"info","openTelemetryClientEnabled":false,"openTelemetryConfigName":"o365","openTelemetryServiceVersion":"25.1.0","openTelemetryUniverse":"prod","openTelemetryCollector":"http://localhost:4318","openTelemetryRouteChangeAllowedTime":"5000","apolloDevToolsEnabled":false,"inboxMuteWipFeatureEnabled":false},"isFallback":false,"isExperimentalCompile":false,"dynamicIds":["./components/community/Navbar/NavbarWidget.tsx","./components/community/Breadcrumb/BreadcrumbWidget.tsx","./components/customComponent/CustomComponent/CustomComponent.tsx","./components/blogs/BlogArticleWidget/BlogArticleWidget.tsx","./components/external/components/ExternalComponent.tsx","./components/messages/MessageView/MessageViewStandard/MessageViewStandard.tsx","./components/messages/ThreadedReplyList/ThreadedReplyList.tsx","../shared/client/components/common/List/UnwrappedList/UnwrappedList.tsx","./components/tags/TagView/TagView.tsx","./components/tags/TagView/TagViewChip/TagViewChip.tsx"],"appGip":true,"scriptLoader":[{"id":"analytics","src":"https://techcommunity.microsoft.com/t5/s/gxcuf89792/pagescripts/1730819800000/analytics.js?page.id=BlogMessagePage&entity.id=board%3Aeducatordeveloperblog&entity.id=message%3A381053","strategy":"afterInteractive"}]}