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
insides
configuration 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
insidetypes
configuration field of theinsides
element mentioned above must exists. The type of the value is a special enum calledInsideType
which 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/XDoorEnter
andRessources/audio/sounds/XDoorExit
whereX
is the string representation of the enum value):Stretch
:StretchDoorEnter
andStretchDoorExit
Slide
:SlideDoorEnter
andSlideDoorExit
Twist
:TwistDoorEnter
andTwistDoorExit
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):
nocolorchange
is false. This is only set to true as part of AreaSpecific in the following areas:BanditHideout
ChomperCaves
SandCastle
overrideskybox
is false. This is unrelated because it's supposed to only prevent the skybox's_Rotation
to change, but it's still required to be false for inside fading- RenderSettings.
skybox
must exists. This is required because theskybox
needs to be faded alongside therender
objects
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 toogmats
when during the cleanup part. However, the materials's enablement will still be updated depending onfadeammount
AlwaysShow
: Therender
will never have its materials's colors changed and it will always remain enabled during the fading, but its sharedMaterials will still be restored toogmats
during 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:
fadeammount
is set to a lerp from the existing one to the destination value with a factor of TieFramerate(fadingSpeed
)- RenderSettings.
skybox
has its_Tint
set to a color where r, g and b arefadeammount
clamped from 0.0 to 0.5 - If
insidedum
is null, it is created as a new MaterialPropertyBlock - Each
render
that isn't null and have anAlwaysShow
tag will be enabled, but their materials color won't change - Each
render
that isn't null and don't have aAlwaysShow
tag will have the following happen:- The
render
enablement is set depending onfadeammount
. It's enabled if it's higher than 0.15 and disabled otherwise - If the
render
doesn't have aNoMapColor
tag and fufills other conditions,insidedim
will have its_Color
set to a color where r, g and b arefadeammount
(the a is the sharedMaterial's color.a) followed byinsidedim
being set on therender
's porperty block. Here are the conditions for arender
material 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
samira
exists, SamiraStop is called on MainManager.events
usingsamira
with instant followed bysamira
being set to null player
entity'semoticon
is removed by playing the-1
clip on it withemotioncooldown
set to 0.0player
'snpc
list is reset to a new list- dynamicFriction of the
player
'scool
material 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 nameXDoorEnter
whereX
is 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,
Center
direction and theplayer
as the caller - Every
playerdata
entity gets rooted and theirrigid
velocity zeroed out - If
chompy
exists, they are childed to this map and theirrigid
velocity 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.
samiramusics
if it wasn't in it already):Calm
Title
Wind
Water
MachineHum
Breathing
- The matching
insides
's Animator gets theOpen
clip played on it if it exists - MainManager.
lastinside
is set to -1 (this field is UNUSED) - instance.
insideid
is 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.camoffset
and instance.camangleoffset
respectively - If the caller's
vectordata[2]
magnitude is above 0.1, instance.camoffset
is set to it (set todefaultcamoffset
otherwise) - If the caller's
vectordata[3]
magnitude is above 0.1, instance.camangleoffset
is set to it (set todefaultcamangle
otherwise) - If
tieinsidedoorentities
is true, all other DoorSameMap inentities
other than the caller have theirvectordata[4]
andvectordata[5]
set to the caller's - Yield all frames until the
player
's entity'sforcemoove
is done - Every
playerdata
entity gets rooted and theirrigid
velocity zeroed out - If
chompy
exists, they are childed to this map and theirrigid
velocity is zeroed out
Exiting an inside¶
- If we aren't
inevent
, the corresponding sound is played which has the nameXDoorExit
whereX
is the string value of the matchinginsidetypes
(the inside id is fetched from the caller'sdata[0]
) - If
music
isn't empty, ChangeMusic is called withmusic[0]
using a 0.2 fadespeed - The matching
insides
's Animator gets theClose
clip played on it if it exists - instance.
insideid
is 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.
camoffset
and instance.camangleoffset
are restored using the caller'svectordata[4]
andvectordata[5]
respectively - Yield all frames until the
player
's entity'sforcemoove
is done
Cleanup¶
This happens regardless if we entered or exited an inside:
- Yield for a frame
player
'slastpos
and its entity'slastpost
both set to its position- dynamicFriction of the
player
'scool
material is set to the value it had before this coroutine - DetectIgnoreSphere called on the
player
's entity which makes theirdetect
collider ignore all NPCControl'sscol
- If a Caravan exists, Refresh is called on the first one found (it plays the
Sleep
orIdle
animation on the Caravan'ssnail
depending on the value ofissleep
) - Yield all frames until MainManager.
musiccoroutine
is null (in case entering the inside caused a music change) - The
minipause
is 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
DelAftBtl
tag are destroyed. This is only to destroy BeetleGrass that are in the process of being faded and desatroyed - If the
player
'sbeemerang
exists, it is destroyed
inside is true (entering an inside)¶
- If there is a caller:
- All
entities
whosenpcdata
.insideid
isn't -2 have their enability changed:- Disabled if thier
npcdata
.inisdeid
doesn't match the caller'sdata[0]
(the inside we are entering) - Otherwise, enabled if
hideinside
is true and thiernpcdata
.inisdeid
match 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
vectordata
contains 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,camlimitpos
is set to it (value saved intcpos
for restore later) - If
vectordata[7]
magnitude is above 0.1,camlimitneg
is set to it (value saved intcneg
for restore later)
- If
- All
- Otherwise (there's no caller):
- All
entities
whosenpcdata
.insideid
doesn'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
insides
has 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
setinsidecenter
is true, instance.camtarget
is set to to the currentinsides
's transform
inside is false (exiting an inside)¶
canlimipos
is restored totcpos
(defaults tooriginallimitpos
if it's null)camlimitneg
is restored totcneg
(defaults tooriginallimitneg
if it's null)- What happends here depends on
hideinsides
:- If it's true:
- All
entities
except DoorSameMap and DoorOtherMap have their enabilitiy changed to be enabled only when theirhideinside
is false and theirnpcdata
.insideid
is -1 (not bound to an inside) - All
insides
are disabled
- All
- Otherwise, if it's false:
- All
entities
that aren'tiskill
have their enabilitiy changed to be enabled only when theirhideinside
is false and theiroldstate
set to -1 (if they have ananim
, its speed is reset to 1.0) - All
insides
are enabled and if they have a Fader, it is enabled too
- All
- If it's true:
- If
setinsidecenter
is true, instance.camtarget
is set to theplayer
Cleanup¶
This happens regardless if we entered or exited an inside:
player
'spausecooldown
is set to 7.0- RefreshEntities called without forceanim and with refreshmap