Insides¶
Maps can define specific locations within the map called an "inside".
An inside is a portion of the game (defined by its root GameObject) that should only be accessible through a DoorSameMap where the inside can be "entered" or "exited" through those NPCControl. It shouldn't in practice be possible to access the inside or getting out of it without passing through a DoorSameMap because the current inside the player is in greatly impacts entities's Update logic meaning violating this rule will cause unexpected behaviors. Essentially, most entities aren't considered active when the current inside doesn't match the inside an entity is defined in. There's also transitions that happens when entering or exiting an inside. They are also treated differently visually where entering an inside will cause the outside to be faded and this is only reversed when exiting the inside.
It should be noted that a DoorSameMap acts as both an entry and an exit point, but an inside may have multiple DoorSameMaps leading in and out of it. As long as it's not possible to access the inside or to get out of it without going through a DoorSameMap, it will behave as expected.
Additionally, it's possible to configure a JumpSpring to use an existing DoorSameMap when jumped on, but in that case, it still ends up doing the same procedure than the player going through a DoorSameMap directly, it's just performed programmaically by the JumpSpring.
At the most basic level, an inside can be considered very close to the concept of "interior", but what makes them special is they are managed in a particular way by the game where it treats the insides differently than anything else (or the "outside").
This system involves multiple configuration fields to define the insides and to configure their behaviors:
| Name | Type | Description | Default |
|---|---|---|---|
| insides | GameObject[] | The list of GameObject in this map that is considered an interior part and will get special treatment during rendering and include special transition managed by DoorSameMap map entities | Empty array |
| insidetypes | InsideType[] | This is meant to be a matching array for insides and each matching element corresponds to the sound that will play when entering or exiting the inside. This field's string value is the prefix to the sound's name: when entering the inside, the name of the sound that will play ends with DoorEnter and when exiting it, it will end with DoorExit. Concatenating this field's string value as the prefix gives the full sound name to play when entering or exiting the inside |
Empty array, but if the length doesn't match insides on Start, it is reset to be an array with the same length, but with all values left to Stretch |
| tieinsidedoorentities | bool | If this is true when entering an inside, every DoorSameMap in the map has their vectordata[4] and vectordata[5] set to the current camoffset / camangleoffset which means no matter how the inside is exited, the camera values will be restored consistently to the values when it was entered |
false |
| hideinsides | bool | If this is true, all insides that doesn't match the current one will be disabled. This will also disable any entity with hideinside set to true whose insideid doesn't match the current one. This is only used in the AntTunnels map to hide the break room part of the map |
false |
| setinsidecenter | bool | If this is true, whenener RefreshInsides is called, instance.camtarget will be set to the current inside's transform after entering one and set back to MainManager.player's transform when exiting one |
false |
| fadingspeed | float | The fraction of TieFramerate to use in FixedUpdate when updating fadeammount when entering or exiting an inside |
0.2 |
Defining an inside¶
In order to define an inside, the following must be done on the map's prefab:
- The root GameObject of the inside must be listed in the
insidesconfiguration field. An inside can only contain a single root GameObject that will also includes its children. This means that if multiple GameObject needs to be part of the inside, they must share a common root GameObject even if it's an empty one. This GameObject must NOT be be located under themainmesh's GameObject because those are what the outside fading will affect so the inside can't be there. It has to sit outside of themainmesh's tree (it can be a sibling to it which is usually what's done) - The index matched of the
insidetypesconfiguration field of theinsideselement mentioned above must exists. The type of the value is a special enum calledInsideTypewhich informs the game which enter and exit sound should play when entering or exiting the inside. It's possible to leave the array empty or to not have its length matchinsides, but doing this will cause all applicable elements to be created and default toStretch. The possible values for this enum are as follows as well as their enter/exit sound they refer to (they are namedRessources/audio/sounds/XDoorEnterandRessources/audio/sounds/XDoorExitwhereXis the string representation of the enum value):Stretch:StretchDoorEnterandStretchDoorExitSlide:SlideDoorEnterandSlideDoorExitTwist:TwistDoorEnterandTwistDoorExit
This is all that's needed to define the inside, but to actually leverage it, at least 1 DoorSameMap must exist whose data[0] is bound to the inside's id. This id corresponds to its insides / insidestypes index. Also, as mentioned earlier, the inside shouldn't be able to be reached or escaped physically without going through a DoorSameMap so the colliders needs to be setup appropriately.
Multiple DoorSameMaps may be defined for the same insides, but if this is the case for at least one inside in the map, it is recommended to also set the tieinsidedoorentities configuration field to true because it allows the camoffset / camangleoffset restore function when exiting the inside to be consistent regardless of how the inside was entered. See the section below for more details.
tieinsidedoorentities¶
This field allows to make the camoffset / camangleoffset restore feature when exiting an inside to work consistently even if the inside has multiple DoorSameMape bound to it.
If it's true when entering an inside, every DoorSameMap in the map has their vectordata[4] and vectordata[5] set to the current camoffset / camangleoffset which means no matter how the inside is exited, the camera values will be restored consistently to the values when it was entered
hideinsides¶
If this field is true, all insides that doesn't match the current one will be disabled. This will also disable any entity with hideinside set to true whose insideid doesn't match the current one.
This is only used in the AntTunnels map to hide the break room part of the map
setinsidecenter¶
If this is true, it overrides the camera system more by setting instance.camtarget to the insides's transform when entering it. It will be reset to the player's transform when exiting the inside
Outside fading¶
During the course of entering or exiting an inside, FixedUpdate can fade in or out RenderSettings.skybox and all render who will also end up being disabled using a tracking field called fadeammount. It is contained in a method called UpdateInsideColor which receives a targetalpha parameter which is the opacity targetted depending if inside or outside (1.0 if outside, 0.0 if inside).
Normally, none of the render should be in any inside so it's effectively fading in and out the "outside". This involves the fadingspeed configuration field which is the fraction of TieFramerate to use in FixedUpdate when updating fadeammount when entering or exiting an inside.
This fading logic is relatively complex because it doesn't always happen and is incorrectly intertwined with other unrelated graphical features. For the fading to happen in general, all the following conditions must be fufilled (they don't do anything in practice however because the maps in which these would apply don't have any insides defined):
nocolorchangeis false. This is only set to true as part of AreaSpecific in the following areas:BanditHideoutChomperCavesSandCastle
overrideskyboxis false. This is unrelated because it's supposed to only prevent the skybox's_Rotationto change, but it's still required to be false for inside fading- RenderSettings.
skyboxmust exists. This is required because theskyboxneeds to be faded alongside therenderobjects
Assuming all the above are fufilled (normally always the case when it matters), there are 2 parts to the fading logic:
- The actual update of the fading (when entering or exiting)
- The cleanup of the fading when fully outside after exiting
Also, render's GameObjects can have tags that changes the behavior of the fading:
NoMapColor: Therender's materials's colors won't change when entering an inside and its sharedMaterials won't be restored toogmatswhen during the cleanup part. However, the materials's enablement will still be updated depending onfadeammountAlwaysShow: Therenderwill never have its materials's colors changed and it will always remain enabled during the fading, but its sharedMaterials will still be restored toogmatsduring the cleanup part
Fading update¶
The fading only updates whenever fadeammount gets further than 0.025 away from the value it should be which is 0.0 if in an inside and 1.0 if not (the targetalpha of UpdateInsideColor).
Essentially, fadeammount is what controls the materials's color to fade in or out. Everytime fadeammount changes, the materials's colors will be set to a color where all r, g and b components will be set to fadeammount (except for the skybox where it's clamped from 0.0 to 0.5).
It effectively means that the colors will change to pure black when entering the inside and back to pure white (which is normal coloring) when exiting the inside (except the skybox where it goes to pure gray).
The destination value (0.0 or 1.0) is constantly updated, but whether any changes happen depens on if fadeammount gets further away then 0.025 from it. It means that the moment instance.insideid changes, FixedUpdate will have the fading happen accordingly on its own.
As for the actual fading, it is done like the following:
fadeammountis set to a lerp from the existing one to the destination value with a factor of TieFramerate(fadingSpeed)- RenderSettings.
skyboxhas its_Tintset to a color where r, g and b arefadeammountclamped from 0.0 to 0.5 - If
insidedumis null, it is created as a new MaterialPropertyBlock - Each
renderthat isn't null and have anAlwaysShowtag will be enabled, but their materials color won't change - Each
renderthat isn't null and don't have aAlwaysShowtag will have the following happen:- The
renderenablement is set depending onfadeammount. It's enabled if it's higher than 0.15 and disabled otherwise - If the
renderdoesn't have aNoMapColortag and fufills other conditions,insidedimwill have its_Colorset to a color where r, g and b arefadeammount(the a is the sharedMaterial's color.a) followed byinsidedimbeing set on therender's porperty block. Here are the conditions for arendermaterial to have its color changed:- The shader isn't
emptymat's shader - The shader isn't
outlinemain's shader - The shader isn't
fakelight - The material has a property named
_Color
- The shader isn't
- The
Inside transition¶
The procedure involved in entering or exiting the inside is done in 3 ways:
- By a DoorSameMap's trigger collider
- By a JumpSpring configured to change insides using an existing DoorSameMap in the map
- Manually by the game (usually done through events)
The first 2 leads to the same path: a coroutine called MoveInside. This method is meant to manage the entire inside transition and it also calls another method called RefreshInsides which is meant to handle the change of instance.insideid. It requires a DoorSameMap to function because it performs the transition using the configuration the DoorSameMap defines.
However, the third way doesn't have to involve MoveInisde. It's possible for the game to take control of the transition such that it can manually set instance.insideid without going through MoveInside. This is because it might not be necessary to involve any DoorSameMap which MoveInside requires. To do this, RefreshInsides needs to be called right after instance.insideid is changed.
The sections below outlines the 2 methods as they are responsible for handling entering or exiting an inside.
MoveInside¶
A coroutine that handles entering or exiting an inside via a DoorSameMap.
public IEnumerator MoveInside(NPCControl caller, bool move)
There's an overload without the move parameter where it default to false. The behavior is the same as it simply start a coroutine with the other one and then yield breaks immediately.
The caller parameter tells the DoorSameMap that causes this inside transition (it cannot be null otherwise, an excpetion will be thrown) and the move parameter tells if the party and its followers should move during the transition or not.
Here's what the coroutine does:
- If
samiraexists, SamiraStop is called on MainManager.eventsusingsamirawith instant followed bysamirabeing set to null playerentity'semoticonis removed by playing the-1clip on it withemotioncooldownset to 0.0player'snpclist is reset to a new list- dynamicFriction of the
player'scoolmaterial is set to 0.0 (the old value is saved for restore later) - CancelAction called on
player - Enter a
minipause
If we aren't in an inside, it means we are entering an inside, otherwise, we are existing it. What happens next depends if we are entering or exiting an inside (the cleanup section happens regardless at the end).
Entering an inside¶
- If we aren't
inevent, the corresponding sound is played which has the nameXDoorEnterwhereXis the string value of the matchinginsidetypes(the inside id is fetched from the caller'sdata[0]) - If move is true:
- TeleportFollowers called with 2.0 distance,
Centerdirection and theplayeras the caller - Every
playerdataentity gets rooted and theirrigidvelocity zeroed out - If
chompyexists, they are childed to this map and theirrigidvelocity is zeroed out
- TeleportFollowers called with 2.0 distance,
- If the caller's
data[1]exists (a Music id is set to play while in this inside):- ChangeMusic is called with the string value of the matching Music with 0.2 fadespeed
- If the music is not among the following list, CheckSamira is called with the Music's string value (this adds the Music to instance.
samiramusicsif it wasn't in it already):CalmTitleWindWaterMachineHumBreathing
- The matching
insides's Animator gets theOpenclip played on it if it exists - MainManager.
lastinsideis set to -1 (this field is UNUSED) - instance.
insideidis set to the inside we are entering which is the caller'sdata[0] - RefreshInsides called with inside and with the caller
- If move is true,
player's entity MoveTowards the caller'svectordata[0]with 1.0 speed, 1 (walk) state and 0 (Idle) stopstate - The caller's
vectordata[4]andvectordata[5]are set to instance.camoffsetand instance.camangleoffsetrespectively - If the caller's
vectordata[2]magnitude is above 0.1, instance.camoffsetis set to it (set todefaultcamoffsetotherwise) - If the caller's
vectordata[3]magnitude is above 0.1, instance.camangleoffsetis set to it (set todefaultcamangleotherwise) - If
tieinsidedoorentitiesis true, all other DoorSameMap inentitiesother than the caller have theirvectordata[4]andvectordata[5]set to the caller's - Yield all frames until the
player's entity'sforcemooveis done - Every
playerdataentity gets rooted and theirrigidvelocity zeroed out - If
chompyexists, they are childed to this map and theirrigidvelocity is zeroed out
Exiting an inside¶
- If we aren't
inevent, the corresponding sound is played which has the nameXDoorExitwhereXis the string value of the matchinginsidetypes(the inside id is fetched from the caller'sdata[0]) - If
musicisn't empty, ChangeMusic is called withmusic[0]using a 0.2 fadespeed - The matching
insides's Animator gets theCloseclip played on it if it exists - instance.
insideidis set to -1 marking that we aren't in an inside anymore - RefreshInsides called without inside and with the caller
- If move is true,
player's entity MoveTowards the caller'svectordata[1]with 1.0 speed, 1 (walk) state and 0 (Idle) stopstate - instance.
camoffsetand instance.camangleoffsetare restored using the caller'svectordata[4]andvectordata[5]respectively - Yield all frames until the
player's entity'sforcemooveis done
Cleanup¶
This happens regardless if we entered or exited an inside:
- Yield for a frame
player'slastposand its entity'slastpostboth set to its position- dynamicFriction of the
player'scoolmaterial is set to the value it had before this coroutine - DetectIgnoreSphere called on the
player's entity which makes theirdetectcollider ignore all NPCControl'sscol - If a Caravan exists, Refresh is called on the first one found (it plays the
SleeporIdleanimation on the Caravan'ssnaildepending on the value ofissleep) - Yield all frames until MainManager.
musiccoroutineis null (in case entering the inside caused a music change) - The
minipauseis ended
RefreshInsides¶
A method that handles updating the map's state after instance.insideid changes. It should always and only be called once that value changes. It is involved in MoveInside described above, but the game can call it manually to perform custom inside transitions.
public void RefreshInsides(bool inside, NPCControl caller)
The inside parameter tells if we are entering an inside (true) or exiting one (false). The caller parameter is optional, but it tells the DoorSameMap being used if applicable.
Here's what the method does:
- All GameObjects with
DelAftBtltag are destroyed. This is only to destroy BeetleGrass that are in the process of being faded and desatroyed - If the
player'sbeemerangexists, it is destroyed
inside is true (entering an inside)¶
- If there is a caller:
- All
entitieswhosenpcdata.insideidisn't -2 have their enability changed:- Disabled if thier
npcdata.inisdeiddoesn't match the caller'sdata[0](the inside we are entering) - Otherwise, enabled if
hideinsideis true and thiernpcdata.inisdeidmatch the caller'sdata[0](the inside we are entering) - If none of the above applies, the enability doesn't change
- Disabled if thier
- If the caller's
vectordatacontains at least 6 elements (assumed to contain 8 if true), it means the caller wants to set camera limits when entering the inside:- If
vectordata[6]magnitude is above 0.1,camlimitposis set to it (value saved intcposfor restore later) - If
vectordata[7]magnitude is above 0.1,camlimitnegis set to it (value saved intcnegfor restore later)
- If
- All
- Otherwise (there's no caller):
- All
entitieswhosenpcdata.insideiddoesn't match the current inside are disabled
- All
- If
hideinsides, allinsides's enability changes to enable only the inside we are entering, the rest gets disabled - If there's a caller:
- All
insideshas the following happen to them:- If the inside doesn't match the caller's
data[0](inside we are entering), it is disabled - Otherwise, if the inside has a Fader, it is disabled
- If the inside doesn't match the caller's
- All
- If
setinsidecenteris true, instance.camtargetis set to to the currentinsides's transform
inside is false (exiting an inside)¶
canlimiposis restored totcpos(defaults tooriginallimitposif it's null)camlimitnegis restored totcneg(defaults tooriginallimitnegif it's null)- What happends here depends on
hideinsides:- If it's true:
- All
entitiesexcept DoorSameMap and DoorOtherMap have their enabilitiy changed to be enabled only when theirhideinsideis false and theirnpcdata.insideidis -1 (not bound to an inside) - All
insidesare disabled
- All
- Otherwise, if it's false:
- All
entitiesthat aren'tiskillhave their enabilitiy changed to be enabled only when theirhideinsideis false and theiroldstateset to -1 (if they have ananim, its speed is reset to 1.0) - All
insidesare enabled and if they have a Fader, it is enabled too
- All
- If it's true:
- If
setinsidecenteris true, instance.camtargetis set to theplayer
Cleanup¶
This happens regardless if we entered or exited an inside:
player'spausecooldownis set to 7.0- RefreshEntities called without forceanim and with refreshmap