mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-24 06:34:09 +00:00
update rules
This commit is contained in:
parent
4c390fdd37
commit
d5522b2fef
8549 changed files with 11783 additions and 1888 deletions
104
.cursor/rules/roblox/en-us/characters/adaptive-animation.md
Normal file
104
.cursor/rules/roblox/en-us/characters/adaptive-animation.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
title: Adaptive Animation
|
||||
description: Set up your character rig to support universal sets of animations and emotes.
|
||||
---
|
||||
|
||||
<Alert severity = 'info'>
|
||||
Adaptive Animation is currently in beta. Enable the beta by navigating to **File** > **Beta Features** and enable **Adaptive Animation**.
|
||||
</Alert>
|
||||
|
||||
Roblox's **Adaptive Animation** system allows animations to play seamlessly between custom characters with unique body types, rigs, and proportions. This feature utilizes a `Class.HumanoidRigDescription` object within your character model that enables you to customize, modify, and map the internal joints of your custom character to enable universal animation support.
|
||||
|
||||
After importing an adaptive animation rig, you can access several Studio tools to help you remap your joints and set a baseline t-pose reference to better support animations. After configuring your rig, your custom character can playback any R15 animation, or any animation that has been published from a character with a `Class.HumanoidRigDescription`.
|
||||
|
||||
<GridContainer numColumns="2">
|
||||
<figure>
|
||||
<img src="../assets/avatar/adaptive-animation/Import-Custom-Humanoid.png" />
|
||||
<figcaption>Seamlessly import custom characters into Studio as a `Custom Humanoid` rig type.</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="../assets/avatar/adaptive-animation/Assign-Joints-A.png" />
|
||||
<figcaption>Modify and adjust the generated `Class.HumanoidRigDescription` to ensure compatibility with universal animations.</figcaption>
|
||||
</figure>
|
||||
</GridContainer>
|
||||
|
||||
## Modify custom character rig
|
||||
|
||||
The following instructions cover the process to [import](#import-model), [assign joints](#assign-joints), and [pose](#create-t-pose) a custom character using a non-standard Roblox rig. When importing a rig as a `Custom Humanoid`, Studio attempts to automatically configure the model's `Class.HumanoidRigDescription`. Use the joint assignment and t-pose tools to fine-tune or adjust the automated changes.
|
||||
|
||||
You can follow along this process with your own custom character, or the same [reference character](../assets/art/reference-files/Snow.fbx) used in this guide.
|
||||
|
||||
<Alert severity = 'info'>
|
||||
This reference character is a modified version of Blender Studio's [Snow](https://studio.blender.org/characters/snow/v4/) character combined with Blender's [base mesh](https://www.blender.org/download/demo-files/) assets.
|
||||
</Alert>
|
||||
|
||||
### Import model
|
||||
|
||||
Use the 3D Importer to import a character model with a HumanoidRigDescription:
|
||||
|
||||
1. Open the 3D Importer file browser by selecting **File** > **Import 3D**.
|
||||
2. Select your rigged character model file.
|
||||
1. Only `.fbx` and `.glTF` formats support rigging data.
|
||||
3. In Rig General, set **Rig Type** to `Custom Humanoid`.
|
||||
<img src="../assets/avatar/adaptive-animation/Import-Custom-Humanoid.png" width = "80%" />
|
||||
4. Select Import. The character should appear in your workspace as a `Class.Model` containing a `Class.HumanoidRigDescription`.
|
||||
|
||||
### Assign joints
|
||||
|
||||
Adaptive animation requires at least 15 of your joints to map to Roblox's standardized 15 joints skeleton. While the automated import process attempts to make this assignment automatically, you can verify and reassign joints by selecting your model's `Class.HumanoidRigDescription` and accessing the joint assignment tool.
|
||||
|
||||
When using the joint assignment tool, a **visual picker** displays in the top right of the 3D viewport displaying the standard Roblox skeleton. To assign joints, you can either click the joints in the 3D viewport or visual picker, then click the corresponding joint in the other window. When a joint is assigned, the joint icon **changes to orange** in both the 3D viewport and the visual picker.
|
||||
|
||||
When using the tool, the following applies:
|
||||
|
||||
- The visual picker displays the 15 standard joint assignments as a filled-in circle.
|
||||
- All 15 standard joints on the visual picker require a joint assignment to your character rig.
|
||||
- The visual picker displays several non-required joint assignments as empty circle outlines. If your rig is compatible with these extra joints, you can assign them appropriately to help improve animation quality.
|
||||
|
||||
<GridContainer numColumns="2">
|
||||
<figure><img src="../assets/avatar/adaptive-animation/Assign-Joints-B.png" />
|
||||
<figcaption>Joints that are not assigned appear as a grey indicator on both the visual picker and the 3D viewport mannequins.</figcaption>
|
||||
</figure>
|
||||
<figure><img src="../assets/avatar/adaptive-animation/Assign-Joints-A.png" />
|
||||
<figcaption>When assigned, joint indicators turn orange in both the visual picker and the 3D viewport.</figcaption>
|
||||
</figure>
|
||||
</GridContainer>
|
||||
|
||||
To access and use the joint assignment tool:
|
||||
|
||||
1. In the Outliner, expand the model you just imported and select the child `Class.HumanoidRigDescription` object. Your character rig and tooling menu displays.
|
||||
<img src="../assets/avatar/adaptive-animation/HRD-Select.png" width = "60%" />
|
||||
2. Select the **Edit** button in the 3D viewport.
|
||||
3. Assign joints by clicking a joint on the body and selecting the corresponding joint in the visual picker. The joint indicator turns orange when assigned.
|
||||
1. Ensure that the 15 mandatory joints, indicated by filled-in circles in the visual picker, are assigned.
|
||||
|
||||
### Create t-pose
|
||||
|
||||
To ensure universal animation support, set up a standard t-pose configuration as a baseline reference for your character. When selecting your `Class.HumanoidRigDescription`, your model is automatically set to a t-pose, but you can use the t-pose tool to modify the position and rotation of your joints to ensure the best animation results.
|
||||
|
||||
<figure>
|
||||
<GridContainer numColumns = "2">
|
||||
<img src="../assets/avatar/adaptive-animation/Tpose-Rotation-A.png" width = "100%"/>
|
||||
<img src="../assets/avatar/adaptive-animation/Tpose-Rotation-B.png" width = "100%"/>
|
||||
</GridContainer>
|
||||
<figcaption> <center>To move joints in the t-pose tool, select the joint and use the multi-transform tool to apply a positional or rotational change. </center></figcaption>
|
||||
</figure>
|
||||
|
||||
To use the t-pose tool to move and rotate joints:
|
||||
|
||||
1. While in the joint assignment tool, select the **t-pose symbol** to switch to t-pose tool.
|
||||
<img src="../assets/avatar/adaptive-animation/Tool-Select.png" />
|
||||
1. If moving both left and right limbs simultaneously, enable the left and right symmetry icon.
|
||||
<img src="../assets/avatar/adaptive-animation/Toolbar.png" />
|
||||
1. Select one of the upper arm joints near the shoulder. Both left and right joints display as blue when selected.
|
||||
1. In some cases, you may also need to position the legs and other areas of the body.
|
||||
<img src="../assets/avatar/adaptive-animation/Tpose-Rotation-A.png" width = "60%"/>
|
||||
1. Use the Rotate tool to move the character into a t-pose.
|
||||
<img src="../assets/avatar/adaptive-animation/Tpose-Rotation-B.png" width = "60%"/>
|
||||
1. When complete, select **Done**.
|
||||
1. In the HRD toolbar, use the dropdown to test and verify various animations. If animations look off, you may need to remap or re-pose your `Class.HumanoidRigDescription`.
|
||||
<img src="../assets/avatar/adaptive-animation/Test-Animation.png" width = "60%"/>
|
||||
|
||||
<Alert severity = 'success'>
|
||||
Once your character's `Class.HumanoidRigDescription` is configured and saved, that character can [support playback](../animation/) of any R15 animation or animations published from a custom character with a `Class.HumanoidRigDescription`.
|
||||
</Alert>
|
||||
275
.cursor/rules/roblox/en-us/characters/appearance.md
Normal file
275
.cursor/rules/roblox/en-us/characters/appearance.md
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
---
|
||||
title: Character appearance
|
||||
description: Customize your in-experience character appearance and properties.
|
||||
---
|
||||
|
||||
Most experiences let players use their own Roblox avatar, although some implement an in-experience customization system like the [UGC Homestore](/resources/templates.md#ugc-homestore) template. Other experiences make limited [modifications](../characters/appearance.md) to player avatars such as helmets, wings, or accessories that match the genre.
|
||||
|
||||
To create a unique experience that alters the appearance of your users, you can customize the default character properties through [avatar settings](#global-avatar-settings) or a [manually modify appearance](#manually-modify-appearance).
|
||||
|
||||
## Global avatar settings
|
||||
|
||||
Studio's **File** ⟩ **Avatar Settings** allows you to quickly set several global player character properties in your experience. These settings apply globally to all player character models joining your experience. To modify specific characters, such as non-player character models, see [manually modify appearance](#manually-modify-appearance).
|
||||
|
||||
In this window, you can set various presets for clothing, accessories, body parts, collision behavior, animations and more. When editing these settings, a preview of the applied settings displays in the workspace.
|
||||
|
||||
For more information, see [Avatar Settings](../studio/avatar-settings.md).
|
||||
|
||||
## Manually modify appearance
|
||||
|
||||
Character models contain a `Class.Humanoid` object that gives the model special characteristics, such as walking, jumping, equipping items, and interacting with the environment.
|
||||
|
||||
You can programmatically modify a `Class.Humanoid` by updating `Class.HumanoidDescription`. This includes player character models or non-player character models in your experience.
|
||||
|
||||
You can adjust the following character properties in your experience using `Class.HumanoidDescription`:
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Character property</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Scale</td>
|
||||
<td>Number values for physical traits `Class.HumanoidDescription.HeightScale|height`, `Class.HumanoidDescription.WidthScale|width`, `Class.HumanoidDescription.HeadScale|head`, `Class.HumanoidDescription.BodyTypeScale|body type` and `Class.HumanoidDescription.ProportionScale|proportion`. This doesn't affect R6 body types.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Accessories </td>
|
||||
<td>The asset IDs of `Class.Accessory|accessories` equipped by a character. </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Classic Clothing</td>
|
||||
<td>The asset IDs of the `Class.Shirt`, `Class.Pants`, and `Class.ShirtGraphic` image textures that you can apply to the character.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Body Part</td>
|
||||
<td>The asset IDs of the `Class.HumanoidDescription.Face|Face`, `Class.HumanoidDescription.Head|Head`, `Class.HumanoidDescription.Torso|Torso`, `Class.HumanoidDescription.RightArm|RightArm`, `Class.HumanoidDescription.LeftArm|LeftArm`, `Class.HumanoidDescription.RightLeg|RightLeg` and `Class.HumanoidDescription.LeftLeg|LeftLeg` parts of a character.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Body Colors</td>
|
||||
<td>The `Class.BodyColors` of the character's individual parts.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Animations</td>
|
||||
<td>The asset IDs of `Class.Animation|Animations` you can use on a character.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
|
||||
Customize a character with `Class.HumanoidDescription` using the following steps:
|
||||
|
||||
1. [Create a description](#create-humanoiddescription) from the user's character, a specific Outfit ID, or from a specific User ID.
|
||||
2. [Modify the description](#modify-humanoiddescription) to customize the properties that you want to apply to the `Class.Humanoid` character.
|
||||
3. [Apply the description](#apply-humanoiddescription) on either a single character, all player characters, or even on all spawning characters.
|
||||
|
||||
<Alert severity="warning">
|
||||
When updating a character's properties through `Class.HumanoidDescription`, it is important to use an up-to-date `Class.HumanoidDescription` of that specific `Class.Humanoid` with `Class.Humanoid:GetAppliedDescription()|Humanoid:GetAppliedDescription()`.
|
||||
</Alert>
|
||||
|
||||
### Create HumanoidDescription
|
||||
|
||||
You can create a new `Class.HumanoidDescription` instance directly within the **Explorer** hierarchy or within a `Class.Script` with the following code:
|
||||
|
||||
```lua title="Create new HumanoidDescription instance"
|
||||
local humanoidDescription = Instance.new("HumanoidDescription")
|
||||
```
|
||||
|
||||
In most cases, you should use an existing `Class.HumanoidDescription` instead of a default new `Class.HumanoidDescription` by referencing an existing player character, avatar outfit, or user ID.
|
||||
|
||||
<br />
|
||||
<h4> From the player character </h4>
|
||||
|
||||
Use the following code sample to create a new `Class.HumanoidDescription` based on the player character's current properties:
|
||||
|
||||
```lua title="Generate a HumanoidDescription from a player character"
|
||||
local humanoid = player.Character and player.Character:FindFirstChildWhichIsA("Humanoid")
|
||||
|
||||
local humanoidDescription = Instance.new("HumanoidDescription")
|
||||
|
||||
if humanoid then
|
||||
humanoidDescription = humanoid:GetAppliedDescription()
|
||||
end
|
||||
```
|
||||
|
||||
<h4> From an existing outfit </h4>
|
||||
|
||||
Use the following sample code to create a `Class.HumanoidDescription` from an outfit ID using `Class.Players:GetHumanoidDescriptionFromOutfitId()|Players.GetHumanoidDescriptionFromOutfitID`:
|
||||
|
||||
```lua title="Generate a HumanoidDescription from an Outfit ID"
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local outfitId = 480059254
|
||||
|
||||
local humanoidDescriptionFromOutfit = Players:GetHumanoidDescriptionFromOutfitId(outfitId)
|
||||
```
|
||||
|
||||
<h4> From a specific user </h4>
|
||||
|
||||
Use the following sample code to create a `Class.HumanoidDescription` from a user ID using `Class.Players:GetHumanoidDescriptionFromUserId()`:
|
||||
|
||||
```lua title="Generate a HumanoidDescription from a user ID"
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local userId = 491243243
|
||||
|
||||
local humanoidDescriptionFromUser = Players:GetHumanoidDescriptionFromUserId(userId)
|
||||
```
|
||||
|
||||
### Modify HumanoidDescription
|
||||
|
||||
To customize `Class.HumanoidDescription` properties, set them directly on the `Class.HumanoidDescription` or use a specified method before applying the `Class.HumanoidDescription` to a character.
|
||||
|
||||
The following code sample provides examples of setting the different types of `Class.HumanoidDescription` properties:
|
||||
|
||||
```lua title="Update various HumanoidDescription properties"
|
||||
local humanoidDescription = Instance.new("HumanoidDescription")
|
||||
humanoidDescription.HatAccessory = "2551510151,2535600138"
|
||||
humanoidDescription.BodyTypeScale = 0.1
|
||||
humanoidDescription.ClimbAnimation = 619521311
|
||||
humanoidDescription.Face = 86487700
|
||||
humanoidDescription.GraphicTShirt = 1711661
|
||||
humanoidDescription.HeadColor = Color3.new(0, 1, 0)
|
||||
```
|
||||
|
||||
<h4> Set multiple accessories </h4>
|
||||
|
||||
For layered or bulk accessory changes, you can use `Class.HumanoidDescription:SetAccessories()` to make accessory related updates. The following code sample adds a layered sweater and jacket in that order to a `Class.HumanoidDescription`:
|
||||
|
||||
```lua title="Change multiple accessories at once"
|
||||
local humanoidDescription = Instance.new("HumanoidDescription")
|
||||
local accessoryTable = {
|
||||
{
|
||||
Order = 1,
|
||||
AssetId = 6984769289,
|
||||
AccessoryType = Enum.AccessoryType.Sweater
|
||||
},
|
||||
{
|
||||
Order = 2,
|
||||
AssetId = 6984767443,
|
||||
AccessoryType = Enum.AccessoryType.Jacket
|
||||
}
|
||||
}
|
||||
|
||||
humanoidDescription:SetAccessories(accessoryTable, false)
|
||||
```
|
||||
|
||||
### Apply HumanoidDescription
|
||||
|
||||
Apply `Class.HumanoidDescription` to specific `Class.Humanoid` characters in your experience with `Class.Humanoid:ApplyDescription()` or `Class.Player:LoadCharacterWithHumanoidDescription()|Humanoid.LoadCharacterWithHumanoidDescription`.
|
||||
|
||||
<Alert severity="warning">
|
||||
Changing the assets on a character while also changing `Class.HumanoidDescription` might lead to undefined behavior.
|
||||
</Alert>
|
||||
|
||||
<h4> On a single character </h4>
|
||||
|
||||
`Class.Humanoid:ApplyDescription()|ApplyDescription()` can target any `Class.Humanoid`. Use the following code to add a new pair of sunglasses and a new torso to the player character:
|
||||
|
||||
```lua title = "Update HumanoidDescription for a specific player character"
|
||||
local humanoid = player.Character and player.Character:FindFirstChildWhichIsA("Humanoid")
|
||||
if humanoid then
|
||||
local descriptionClone = humanoid:GetAppliedDescription()
|
||||
descriptionClone.Torso = 86500008
|
||||
-- Multiple face accessory assets are allowed in a comma-separated string
|
||||
descriptionClone.FaceAccessory = descriptionClone.FaceAccessory .. ",2535420239"
|
||||
-- Apply modified "descriptionClone" to humanoid
|
||||
humanoid:ApplyDescription(descriptionClone)
|
||||
end
|
||||
```
|
||||
|
||||
<h4> On all player characters </h4>
|
||||
|
||||
Use the following sample code to apply a `Class.HumanoidDescription` to all current players in the game:
|
||||
|
||||
```lua title = "Update HumanoidDescription for all current player characters"
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
for _, player in Players:GetPlayers() do
|
||||
local humanoid = player.Character and player.Character:FindFirstChildWhichIsA("Humanoid")
|
||||
if humanoid then
|
||||
-- Create a HumanoidDescription
|
||||
local humanoidDescription = Instance.new("HumanoidDescription")
|
||||
humanoidDescription.HatAccessory = "2551510151,2535600138"
|
||||
humanoidDescription.BodyTypeScale = 0.1
|
||||
humanoidDescription.ClimbAnimation = 619521311
|
||||
humanoidDescription.Face = 86487700
|
||||
humanoidDescription.GraphicTShirt = 1711661
|
||||
humanoidDescription.HeadColor = Color3.new(0, 1, 0)
|
||||
humanoid:ApplyDescription(humanoidDescription)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
<h4> On all spawning characters </h4>
|
||||
|
||||
Use the following sample code to set a specific `Class.HumanoidDescription` for all spawning player characters:
|
||||
|
||||
```lua title = "Update HumanoidDescription for all spawning characters"
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
-- Stop automatic spawning so it can be done in the "PlayerAdded" callback
|
||||
Players.CharacterAutoLoads = false
|
||||
|
||||
local function onPlayerAdded(player)
|
||||
-- Create a HumanoidDescription
|
||||
local humanoidDescription = Instance.new("HumanoidDescription")
|
||||
humanoidDescription.HatAccessory = "2551510151,2535600138"
|
||||
humanoidDescription.BodyTypeScale = 0.1
|
||||
humanoidDescription.ClimbAnimation = 619521311
|
||||
humanoidDescription.Face = 86487700
|
||||
humanoidDescription.GraphicTShirt = 1711661
|
||||
humanoidDescription.HeadColor = Color3.new(0, 1, 0)
|
||||
|
||||
-- Spawn character with the HumanoidDescription
|
||||
player:LoadCharacterWithHumanoidDescription(humanoidDescription)
|
||||
end
|
||||
|
||||
-- Connect "PlayerAdded" event to "onPlayerAdded()" function
|
||||
Players.PlayerAdded:Connect(onPlayerAdded)
|
||||
```
|
||||
|
||||
If the `Class.HumanoidDescription` instance was created in the **Explorer** and parented to the workspace, use the following sample code in a `Class.Script` to access the workspace instance:
|
||||
|
||||
```lua
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
-- Stop automatic spawning so it can be done in the "PlayerAdded" callback
|
||||
Players.CharacterAutoLoads = false
|
||||
|
||||
local function onPlayerAdded(player)
|
||||
-- Spawn character with "workspace.StudioHumanoidDescription"
|
||||
player:LoadCharacterWithHumanoidDescription(workspace.StudioHumanoidDescription)
|
||||
end
|
||||
|
||||
-- Connect "PlayerAdded" event to "onPlayerAdded()" function
|
||||
Players.PlayerAdded:Connect(onPlayerAdded)
|
||||
```
|
||||
|
||||
## Layered clothing on non-R15
|
||||
|
||||
Caged accessories, like layered clothing, use `Class.WrapTarget` and `Class.WrapLayer` to stretch and wrap over a target `Class.Model`. Layered accessories can work with both standard [R15 Roblox characters](../characters/index.md#avatar-characters) and non-R15 models.
|
||||
|
||||
Custom implementation of layered clothing, such as a model using a unique cage UV map, cannot be uploaded and published to the Marketplace. For more information, see [Layered clothing specifications](../art/accessories/clothing-specifications.md).
|
||||
|
||||
Whether you are implementing layered accessories on an avatar R15 rig, or using a custom rig, ensure that your accessories and bodies include the following:
|
||||
|
||||
- The target model, typically the body, has a `Class.WrapTarget` component on the meshes that additional models are intended to wrap around.
|
||||
- The layering model, typically the clothing or accessory, has a `Class.WrapLayer` component on the meshes meant to wrap the target model.
|
||||
- The **outer cage of the target model**, and the **inner and outer cage of the layering model** have matching UV maps.
|
||||
- The corresponding vertices on the target cage should have the same UVs as those vertices on the layer cage.
|
||||
- If your target model is not R15, or doesn't include a `Class.Humanoid`, you must add a `Class.Weld` object to the layering `Class.MeshPart`.
|
||||
- The `Class.Weld` must have `Part0` and `Part1` set to link the layering MeshPart to the Part hierarchy of the Model. For example, `Part0` refers to the accessory and `Part1` refers to the parent Part.
|
||||
- If your target model is both R15 and includes a `Class.Humanoid`, this weld is created automatically.
|
||||
|
||||
<Alert severity = 'info'>
|
||||
See the following resources for additional information on layered clothing and caging:
|
||||
|
||||
- [Layered clothing caging overview](../art/accessories/layered-clothing.md#inner-and-outer-cages)
|
||||
- [Layered clothing caging best practices](../art/accessories/caging-best-practices.md)
|
||||
- [Layered clothing cage mesh specifications](../art/accessories/clothing-specifications.md#cage-meshes)
|
||||
- [Avatar body cage specifications](../art/characters/specifications.md#outer-cages)
|
||||
|
||||
</Alert>
|
||||
90
.cursor/rules/roblox/en-us/characters/emotes.md
Normal file
90
.cursor/rules/roblox/en-us/characters/emotes.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
title: Emotes
|
||||
description: Emotes are character animations used through chat commands, or accessing the emotes menu.
|
||||
---
|
||||
|
||||
Emotes are expressive character [animations](../animation/index.md) that are accessible by using chat commands ("/e cheer") or by accessing the **emotes menu** on the top right of any experience. All users have access to default emotes, such as **dance**, **point**, and **cheer**. Additional avatar emotes can be purchased and equipped from the [Marketplace](https://www.roblox.com/catalog).
|
||||
|
||||
<video src="../assets/avatar/avatar-emotes/Avatar-Emotes.mp4" controls width="100%"></video>
|
||||
|
||||
In your experience, you can perform the following emote customizations:
|
||||
|
||||
- [Open and close](#open-and-close) a user's emotes menu programmatically.
|
||||
- [Add or remove](#add-and-remove-emotes) emotes options from a user's menu.
|
||||
- [Disable](#disable) access to the menu.
|
||||
- [Play](#play-emotes) an emote, targeting a specific user character.
|
||||
|
||||
## Emotes menu
|
||||
|
||||
You can open and close a user's emote menu manually, customize the menu to display specific emotes, or disable the menu completely.
|
||||
|
||||
### Open and close
|
||||
|
||||
To manually open or close a player's emote menu, call `Class.GuiService:SetEmotesMenuOpen()` with a boolean value of true or false.
|
||||
|
||||
The following code sample will open the emotes menu for the user:
|
||||
|
||||
```lua
|
||||
-- Open the emote Menu
|
||||
local GuiService = game:GetService("GuiService")
|
||||
GuiService:SetEmotesMenuOpen(true)
|
||||
```
|
||||
|
||||
If you need to detect whether the emotes menu is open, call `Class.GuiService:GetEmotesMenuOpen()`. This returns a boolean indicating the menu's current state.
|
||||
|
||||
### Add and remove emotes
|
||||
|
||||
Customize the emote menu by setting emotes from the catalog and then equipping emotes to a `Class.Humanoid`. Set emotes with the `Class.HumanoidDescription:SetEmotes()` method and equip up to 8 emotes to the emotes menu using `Class.HumanoidDescription:SetEquippedEmotes()`.
|
||||
|
||||
Use the following code sample in a `Class.LocalScript` within the `Class.StarterCharacterScripts` folder to set and equip emotes in your experience:
|
||||
|
||||
```lua
|
||||
local Players = game:GetService("Players")
|
||||
local humanoid = Players.LocalPlayer.Character.Humanoid
|
||||
local humanoidDescription = humanoid.HumanoidDescription
|
||||
|
||||
-- Set custom emotes within a table
|
||||
local emoteTable = {
|
||||
["Hello"] = {3576686446},
|
||||
["Stadium"] = {3360686498},
|
||||
["Tilt"] = {3360692915},
|
||||
["Shrug"] = {3576968026},
|
||||
["Salute"] = {3360689775},
|
||||
["Point"] = {3576823880}
|
||||
}
|
||||
humanoidDescription:SetEmotes(emoteTable)
|
||||
|
||||
-- Equip emotes in a specific order
|
||||
local equippedEmotes = {"Hello", "Stadium", "Tilt", "Shrug", "Salute", "Point"}
|
||||
humanoidDescription:SetEquippedEmotes(equippedEmotes)
|
||||
```
|
||||
|
||||
<Alert severity="info">
|
||||
Key names, such as "Shrug" or "Salute" are customizable and will appear to the player as written. Emotes will also show up in the order they were set, starting at the top of the wheel and continuing clockwise.
|
||||
</Alert>
|
||||
|
||||
### Disable
|
||||
|
||||
Disable the emotes menu with `Class.StarterGui:SetCoreGuiEnabled()`. Disabling the emotes menu will not prevent emotes from being performed with a chat command.
|
||||
|
||||
The following sample code will disable the emotes menu:
|
||||
|
||||
```lua
|
||||
local StarterGui = game:GetService("StarterGui")
|
||||
StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.EmotesMenu, false)
|
||||
```
|
||||
|
||||
In addition to disabling the menu, you can disable loading of user-owned emotes by setting the `Class.StarterPlayer.UserEmotesEnabled` property within **StarterPlayer** ⟩ **Character** to **false**. This specific property can only be set in Studio and cannot be set by scripts.
|
||||
|
||||
## Play emotes
|
||||
|
||||
To manually play an emote that a character has in its `Class.HumanoidDescription`, call `Class.Humanoid:PlayEmote()`, passing the string name of the emote. This call will return true to indicate that the emote was played successfully, or false otherwise.
|
||||
|
||||
Use the following code sample to play the Shrug emote:
|
||||
|
||||
```lua
|
||||
local Players = game:GetService("Players")
|
||||
local humanoid = Players.LocalPlayer.Character.Humanoid
|
||||
|
||||
humanoid:PlayEmote("Shrug")
|
||||
```
|
||||
75
.cursor/rules/roblox/en-us/characters/index.md
Normal file
75
.cursor/rules/roblox/en-us/characters/index.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
title: Characters
|
||||
description: Characters are interactive models that interact with the world or other users.
|
||||
---
|
||||
|
||||
**Characters** typically refer to any `Class.Model` objects that interact with the world or other users. While a character can be as simple as a glowing sphere that communicates and interacts with users, characters are often human-like models with additional means of expression to encourage immersion and realism.
|
||||
|
||||
Characters can range between **basic** characters, such as a simple non-player character (NPC), or **avatar** characters, which are user-controlled models that include advanced features for movement, animation, and cosmetics.
|
||||
|
||||
All Roblox users are associated with an account-based avatar character. Along with this avatar character, Roblox represents users as **players** in the data model, giving developers access to additional character customization properties, social features, and relevant gameplay and account information. For more information on account specific player features, see [Players](../players/index.md).
|
||||
|
||||
## Basic characters
|
||||
|
||||
Basic characters are often used as NPCs and typically perform one or two simple tasks. A common component of basic characters include a display name, health, and basic movement.
|
||||
|
||||
You can use the following components within your `Class.Model` object to enable these basic features:
|
||||
|
||||
- A group of parts, or [assembly](../physics/assemblies.md), that includes the following:
|
||||
- A collection with the name `HumanoidRootPart` to indicate the root part of the assembly.
|
||||
- A part with the name `Head` to display a display name over.
|
||||
- Additional parts that make up the cosmetic individual body parts, which commonly include the 6 (R6) or 15 (R15) body parts used for human-like models.
|
||||
- Joints, such as `Class.Bone` or `Class.Motor6D`, that connects each body part as an assembly.
|
||||
- A `Class.Humanoid` instance to quickly add common character properties to a model.
|
||||
|
||||
<GridContainer numColumns="2">
|
||||
<figure>
|
||||
<img src="../assets/avatar/character-customization/R6-Example.jpg" />
|
||||
<figcaption>Basic Character Example (R6)</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="../assets/avatar/character-customization/Basic-Character-Model-Data.png" />
|
||||
<figcaption>Data Model</figcaption>
|
||||
</figure>
|
||||
</GridContainer>
|
||||
|
||||
With these components, the character model displays a `Class.Humanoid.DisplayName`, has health, and can move with `Class.Humanoid.Move()`. For more information on additional configurations of the display and health elements, see [Name/health display](../characters/name-health-display.md).
|
||||
|
||||
<Alert severity = 'info'>
|
||||
For information on implementing layered clothing on a non-R15 character model, see [Character appearance](./appearance.md#layered-clothing-on-non-r15).
|
||||
</Alert>
|
||||
|
||||
## Avatar characters
|
||||
|
||||
Avatar characters use a standardized set of character components that allow you to use body and facial animations, and equip clothing and accessories. By standardizing the components, all avatar character models can access these features, whether they are controlled by a player or an NPC.
|
||||
|
||||
By default, all users join experiences as their saved Roblox avatar, which already includes all the components for an avatar character. A complete avatar character has the following components in their `Class.Model`:
|
||||
|
||||
- An [assembly](../physics/assemblies.md) of `Class.MeshPart` objects, that includes the following:
|
||||
- A part with the name `HumanoidRootPart` to indicate the root part of the assembly.
|
||||
- A part with the name `Head` to display a display name over.
|
||||
- Additional parts that make up the cosmetic individual body parts, that must include the standard 15 body parts used for R15 human-like models.
|
||||
- Joints, such as `Class.Bone` or `Class.Motor6D`, that connects each part and follows a standard joint hierarchy.
|
||||
- A `Class.Humanoid` instance to implement and access common character properties.
|
||||
- `Class.WrapLayer` objects for each of your model's body parts, to enable it to wear clothing and other layerable cosmetics.
|
||||
- `Class.FaceControls` to enable facial expressions and poses for your character's head.
|
||||
- `Class.Attachment` objects for each of the standard attachment points on the character body.
|
||||
|
||||
<GridContainer numColumns="2">
|
||||
<figure>
|
||||
<img src="../assets/avatar/character-customization/R15-Example.jpg" />
|
||||
<figcaption>Avatar Character Example (R15)</figcaption>
|
||||
</figure>
|
||||
<figure>
|
||||
<img src="../assets/avatar/character-customization/Avatar-Character-Model-Data.png" />
|
||||
<figcaption>Data Model</figcaption>
|
||||
</figure>
|
||||
</GridContainer>
|
||||
|
||||
<Alert severity = 'info'>
|
||||
If you are creating your own custom avatar character, you must configure your character model's components in a third-party modeling tool like Blender or Maya before importing the model into Studio.
|
||||
|
||||
See [Avatars](../art/characters/index.md) for information regarding the creation of avatar components, creation guides, and marketplace information.
|
||||
</Alert>
|
||||
|
||||
For information on modifying and customizing existing characters in your experience, see [Character appearance](../characters/appearance.md).
|
||||
221
.cursor/rules/roblox/en-us/characters/name-health-display.md
Normal file
221
.cursor/rules/roblox/en-us/characters/name-health-display.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
---
|
||||
title: Character name/health display
|
||||
description: You can customize character UI, like name and health displays, using Class.Humanoid.
|
||||
---
|
||||
|
||||
The `Class.Humanoid` instance is used to create character models, both for user avatars and NPCs. When a `Class.Humanoid` is present inside a `Class.Model` that contains a part named **Head**, Roblox displays a name and/or health bar above that part.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/Display-Indicated.jpg" width="800" alt="Character display information above an in-experience avatar" />
|
||||
|
||||
Through various `Class.Humanoid` properties, you can modify the following:
|
||||
|
||||
- The [distance](#display-distance-type) from which users can see the name/health of other humanoids in relation to their own character's humanoid.
|
||||
- The [display name](#override-display-names) which shows over a humanoid.
|
||||
- Whether a humanoid's [health bar](#health-display-type) always appears, never appears, or only appears when the humanoid is damaged.
|
||||
- Whether names and health bars are [occluded](#occlusion) (hidden) when line-of-sight between the camera and another humanoid is blocked.
|
||||
|
||||
<Alert severity="warning">
|
||||
As noted in the introduction, character name/health display requires that the `Class.Humanoid` instance is inside a `Class.Model` and that the model contains a `Class.BasePart` named **Head**. Both objects must also be at the same level in the model's hierarchy.
|
||||
</Alert>
|
||||
|
||||
## Display properties
|
||||
|
||||
### Display distance type
|
||||
|
||||
The `Class.Humanoid.DisplayDistanceType` property sets how users see the name/health of other characters in relation to their own character.
|
||||
|
||||
#### Viewer
|
||||
|
||||
When a humanoid's `Class.Humanoid.DisplayDistanceType|DisplayDistanceType` is set to `Enum.HumanoidDisplayDistanceType|HumanoidDisplayDistanceType.Viewer`, it sees the name/health of other humanoids within range of its own `Class.Humanoid.NameDisplayDistance|NameDisplayDistance` and `Class.Humanoid.HealthDisplayDistance|HealthDisplayDistance`. You can consider this the lowest priority since it is not taken into account for other humanoids configured as [subject](#subject) or [none](#none).
|
||||
|
||||
In the following scenario, the user's character (**Viewer**) has a larger `Class.Humanoid.NameDisplayDistance|NameDisplayDistance` than `Class.Humanoid.HealthDisplayDistance|HealthDisplayDistance`, as indicated by the circles. As a result, the user sees character names for both **Watchman** and **Octavia**, but only sees a health bar for **Watchman**.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/DisplayDistanceType-Viewer.jpg" width="90%" />
|
||||
|
||||
#### Subject
|
||||
|
||||
When a humanoid's `Class.Humanoid.DisplayDistanceType|DisplayDistanceType` is set to `Enum.HumanoidDisplayDistanceType|HumanoidDisplayDistanceType.Subject`, it takes **full control** over its own name and health display through its `Class.Humanoid.NameDisplayDistance|NameDisplayDistance` and `Class.Humanoid.HealthDisplayDistance|HealthDisplayDistance` values. Effectively, other humanoids will only see the subject's name/health within those distances from the **subject** humanoid.
|
||||
|
||||
In the following scenario, both **Watchman** and **Octavia** are set to **Subject** and their `Class.Humanoid.NameDisplayDistance|NameDisplayDistance` ranges are indicated by the circles. Only the name **Octavia** is seen by the user whose character is standing inside her range but outside the **Watchman** humanoid's range.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/DisplayDistanceType-Subject.jpg" width="90%" />
|
||||
|
||||
#### None
|
||||
|
||||
When a humanoid's `Class.Humanoid.DisplayDistanceType|DisplayDistanceType` is set to `Enum.HumanoidDisplayDistanceType|HumanoidDisplayDistanceType.None`, its name and health bar do not appear under any circumstances. In the following scenario, both **Watchman** and **Octavia** are set to **None**, so the other character does not see their name or health even when in range.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/DisplayDistanceType-None.jpg" width="90%" />
|
||||
|
||||
### Health display type
|
||||
|
||||
The `Class.Humanoid.HealthDisplayType` property provides further control over the character's health bar visibility. The bar reflects the humanoid's `Class.Humanoid.Health|Health` as a factor of its `Class.Humanoid.MaxHealth|MaxHealth` and it changes color from green to yellow to red as the humanoid's health decreases.
|
||||
|
||||
#### AlwaysOn
|
||||
|
||||
When a humanoid's `Class.Humanoid.HealthDisplayType|HealthDisplayType` is set to `Enum.HumanoidHealthDisplayType|HumanoidHealthDisplayType.AlwaysOn`, its health bar always appears.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/HealthDisplayType-AlwaysOn.jpg" width="90%" />
|
||||
|
||||
#### DisplayWhenDamaged
|
||||
|
||||
A humanoid with `Class.Humanoid.HealthDisplayType|HealthDisplayType` set to `Enum.HumanoidHealthDisplayType|HumanoidHealthDisplayType.DisplayWhenDamaged` only shows a health bar when its `Class.Humanoid.Health|Health` is less than its `Class.Humanoid.MaxHealth|MaxHealth`. In the following scenario, **Watchman** has full health and does not display a health bar, but **Octavia** is damaged by 50% and displays a yellow health bar.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/HealthDisplayType-DisplayWhenDamaged.jpg" width="90%" />
|
||||
|
||||
#### AlwaysOff
|
||||
|
||||
When a humanoid's `Class.Humanoid.HealthDisplayType|HealthDisplayType` is set to `Enum.HumanoidHealthDisplayType|HumanoidHealthDisplayType.AlwaysOff`, its health bar never appears under any circumstances.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/HealthDisplayType-AlwaysOff.jpg" width="90%" />
|
||||
|
||||
### Occlusion
|
||||
|
||||
Occlusion (hiding) of humanoid names behind walls or other objects is controlled by the character's `Class.Humanoid.NameOcclusion` property.
|
||||
|
||||
<Alert severity="warning">
|
||||
Occlusion does not occur if the occluding object's `Class.BasePart.Transparency|Transparency` is higher than 0.99. Occlusion also does not occur when a humanoid is hidden behind a `Class.Model` containing a `Class.Humanoid`, such as another user's avatar character.
|
||||
</Alert>
|
||||
|
||||
#### NoOcclusion
|
||||
|
||||
When a humanoid is hidden behind a visible object and its `Class.Humanoid.NameOcclusion|NameOcclusion` is set to `Enum.NameOcclusion|NameOcclusion.NoOcclusion`, its name and health are never occluded from viewing humanoids.
|
||||
|
||||
In the following scenario, both **Watchman** and **Octavia** are set to **NoOcclusion**. Although both are sufficiently hidden behind stone columns, the viewing humanoid still sees their name/health displays.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/NameOcclusion-NoOcclusion.jpg" width="90%" />
|
||||
|
||||
#### OccludeAll
|
||||
|
||||
When a humanoid is hidden behind a visible object and its `Class.Humanoid.NameOcclusion|NameOcclusion` is set to `Enum.NameOcclusion|NameOcclusion.OccludeAll`, its name and health are always occluded from viewing humanoids.
|
||||
|
||||
In the following scenario, both **Watchman** and **Octavia** are sufficiently hidden behind stone columns. **Watchman** is set to **OccludeAll**, so its name and health are hidden from the viewing humanoid. **Octavia**, however, is set to [NoOcclusion](#noocclusion) and her name/health remains visible to the viewing humanoid.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/NameOcclusion-OccludeAll.jpg" width="90%" />
|
||||
|
||||
#### EnemyOcclusion
|
||||
|
||||
When a humanoid is hidden behind a visible object and its `Class.Humanoid.NameOcclusion|NameOcclusion` is set to `Enum.NameOcclusion|NameOcclusion.EnemyOcclusion`, its name and health are only occluded from enemy humanoids (players on a different `Class.Team`).
|
||||
|
||||
In the following scenario, both **Watchman** and **Octavia** are sufficiently hidden behind stone columns, and both are set to **EnemyOcclusion**. The viewing humanoid and **Watchman** are on the same `Class.Team`, so name/health occlusion does not occur. However, the name and health of **Octavia**, on the opposing team, are occluded.
|
||||
|
||||
<img src="../assets/avatar/name-health-display/NameOcclusion-EnemyOcclusion.jpg" width="90%" />
|
||||
|
||||
## Modify character displays
|
||||
|
||||
### User avatars
|
||||
|
||||
To modify the name or health display for every incoming avatar in an experience, connect the `Class.Players.PlayerAdded` and `Class.Player.CharacterAdded` events in a `Class.Script` and set [display properties](#display-properties) on the character's `Class.Humanoid`.
|
||||
|
||||
```lua title="Script - Global Customization" highlight="3,4,8,10,12,14,19"
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local function onPlayerAdded(player)
|
||||
player.CharacterAdded:Connect(function(character)
|
||||
local humanoid = character:FindFirstChildWhichIsA("Humanoid")
|
||||
if humanoid then
|
||||
-- Give each humanoid full control over its name/health display distance
|
||||
humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.Subject
|
||||
-- Set name display distance to 20 studs
|
||||
humanoid.NameDisplayDistance = 20
|
||||
-- Set health bar display distance to 15 studs
|
||||
humanoid.HealthDisplayDistance = 15
|
||||
-- Only show health bar when humanoid is damaged
|
||||
humanoid.HealthDisplayType = Enum.HumanoidHealthDisplayType.DisplayWhenDamaged
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
Players.PlayerAdded:Connect(onPlayerAdded)
|
||||
```
|
||||
|
||||
You can also customize properties based on a player's `Class.Team`, such as setting all "guard" players to a generic name, and hiding the names of all "ninja" players.
|
||||
|
||||
<span id="team-customization-script"></span>
|
||||
|
||||
```lua title="Script - Team Customization" highlight="9,12"
|
||||
local Players = game:GetService("Players")
|
||||
|
||||
local function onPlayerAdded(player)
|
||||
player.CharacterAdded:Connect(function(character)
|
||||
local humanoid = character:FindFirstChildWhichIsA("Humanoid")
|
||||
if humanoid then
|
||||
-- Set the name of all guards to generic "Guard"
|
||||
if player.Team.Name == "Guards" then
|
||||
humanoid.DisplayName = "Guard"
|
||||
-- Hide the name for all ninjas
|
||||
elseif player.Team.Name == "Ninjas" then
|
||||
humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
Players.PlayerAdded:Connect(onPlayerAdded)
|
||||
```
|
||||
|
||||
<Alert severity="info">
|
||||
When a player is assigned to a `Class.Team`, their character's name appears in the same color as the team's `Class.Team.TeamColor|TeamColor`. This helps identify teammates and opposing players in team-based experiences.
|
||||
</Alert>
|
||||
|
||||
### NPC characters
|
||||
|
||||
For NPC characters already placed in the 3D world, you can edit name/health directly on the `Class.Humanoid` object in the [Properties](../studio/properties.md) window.
|
||||
|
||||
<GridContainer numColumns="2">
|
||||
<img src="../assets/avatar/name-health-display/Customize-NPC.jpg" />
|
||||
<img src="../assets/avatar/name-health-display/Humanoid-Properties.png" />
|
||||
</GridContainer>
|
||||
|
||||
## Override display names
|
||||
|
||||
By default, a humanoid's display name matches the user's Roblox account **Display Name** which is unique and separate from their account **Username**. To show a fully custom name that's unrelated to the user's account, you can override the `Class.Humanoid.DisplayName` property.
|
||||
|
||||
### Set directly
|
||||
|
||||
You can set the `Class.Humanoid.DisplayName|DisplayName` property of any `Class.Humanoid` instance which you gain reference to through a `Class.Script`, such as the [team customization](#user-avatars) example, or directly on an [NPC](#npc-characters) character's **Humanoid** object.
|
||||
|
||||
### Set through user input
|
||||
|
||||
In some genres like roleplaying or fighting, you may want to provide a method for users to input their own character name, pet character name, etc. that's specific to the experience and isn't tied to their account display name. You can gather this input on the client side through a `Class.TextBox` name entry.
|
||||
|
||||
<img src="../assets/ui/ui-objects/TextBox-Example.jpg" width="840" />
|
||||
|
||||
Once the input is submitted, you can pass it to the server through a [remote event](../scripting/events/remote.md) and then, on the server side, listen for the remote event and assign the [filtered](../ui/text-filtering.md) name to the user character's `Class.Humanoid`.
|
||||
|
||||
```lua title="LocalScript - Fire Remote Event" highlight="3,4"
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local changeNameEvent = ReplicatedStorage:WaitForChild("ChangeNameEvent")
|
||||
changeNameEvent:FireServer("Amory")
|
||||
```
|
||||
|
||||
```lua title="Script - Assign Filtered Name" highlight="15,20,23,28"
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
-- Create remote event to receive text from client for filtering
|
||||
local changeNameEvent = Instance.new("RemoteEvent")
|
||||
changeNameEvent.Name = "ChangeNameEvent"
|
||||
changeNameEvent.Parent = ReplicatedStorage
|
||||
|
||||
local function onRequestNameChange(player, newName)
|
||||
local character = player.Character
|
||||
local humanoid = character:FindFirstChildWhichIsA("Humanoid")
|
||||
|
||||
local filterResult
|
||||
local success, errorMessage = pcall(function()
|
||||
filterResult = TextService:FilterStringAsync(newName, player.UserId)
|
||||
end)
|
||||
if success then
|
||||
local filteredName
|
||||
local success, errorMessage = pcall(function()
|
||||
filteredName = filterResult:GetNonChatStringForBroadcastAsync()
|
||||
end)
|
||||
if success and humanoid then
|
||||
humanoid.DisplayName = filteredName
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
changeNameEvent.OnServerEvent:Connect(onRequestNameChange)
|
||||
```
|
||||
633
.cursor/rules/roblox/en-us/characters/pathfinding.md
Normal file
633
.cursor/rules/roblox/en-us/characters/pathfinding.md
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
---
|
||||
title: Character pathfinding
|
||||
description: Pathfinding is the process of moving a character along a logical path to reach a destination.
|
||||
---
|
||||
|
||||
**Pathfinding** is the process of moving a character along a logical path to reach a destination, avoiding obstacles and (optionally) hazardous materials or defined regions.
|
||||
|
||||
<video controls src="../assets/avatar/pathfinding/Showcase.mp4" width="100%" alt="Video showcase of character pathfinding across a series of bridges"></video>
|
||||
|
||||
## Navigation visualization
|
||||
|
||||
To assist with pathfinding layout and debugging, Studio can render a navigation mesh and [modifier](#pathfinding-modifiers) labels. To enable them, toggle on **Navigation mesh** and **Pathfinding modifiers** from the [Visualization Options](../studio/ui-overview.md#visualization-options) widget in the upper‑right corner of the 3D viewport.
|
||||
|
||||
<img src="../assets/studio/general/Visualization-Options.png" width="780" alt="A close up view of the 3D viewport with the Visualization Options button indicated in the upper-right corner." />
|
||||
|
||||
With **Navigation mesh** enabled, colored areas show where a character might walk or swim, while non-colored areas are blocked. The small arrows indicate areas that a character will attempt to reach by jumping, assuming you set `AgentCanJump` to `true` when [creating the path](#create-paths).
|
||||
|
||||
<img src="../assets/avatar/pathfinding/Navigation-Mesh.jpg" width="800" alt="Navigation mesh showing in Studio" />
|
||||
|
||||
With **Pathfinding modifiers** enabled, text labels indicate specific materials and regions that are taken into consideration when using [pathfinding modifiers](#pathfinding-modifiers).
|
||||
|
||||
<img src="../assets/avatar/pathfinding/Navigation-Labels.jpg" width="800" alt="Navigation labels showing on navigation mesh" />
|
||||
|
||||
## Known limitations
|
||||
|
||||
Pathfinding features specific limitations to ensure efficient processing and optimal performance.
|
||||
|
||||
### Vertical placement limit
|
||||
|
||||
Pathfinding calculations consider only parts within certain vertical boundaries:
|
||||
|
||||
- Lower Boundary — Parts with a bottom **Y** coordinate less than -65,536 studs are ignored.
|
||||
- Upper Boundary — Parts with a top **Y** coordinate exceeding 65,536 studs are ignored.
|
||||
- Vertical Span — The vertical distance from the lowest part's bottom **Y** coordinate to the highest part's top **Y** coordinate must not exceed 65,536 studs; otherwise, the pathfinding system will ignore those parts during the pathfinding computation.
|
||||
|
||||
### Search distance limitation
|
||||
|
||||
The direct line-of-sight distance for pathfinding from the start to the finish point must not exceed 3,000 studs. Exceeding this distance will result in a `Enum.PathStatus|NoPath` status.
|
||||
|
||||
## Create paths
|
||||
|
||||
Pathfinding is initiated through `Class.PathfindingService` and its `Class.PathfindingService:CreatePath()|CreatePath()` function.
|
||||
|
||||
```lua title="LocalScript" highlight="1, 3"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
|
||||
local path = PathfindingService:CreatePath()
|
||||
```
|
||||
|
||||
`Class.PathfindingService:CreatePath()|CreatePath()` accepts an optional table of parameters which fine tune how the character (agent) moves along the path.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Description</th>
|
||||
<th>Type</th>
|
||||
<th>Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>`AgentRadius`</td>
|
||||
<td>Agent radius, in studs. Useful for determining the minimum separation from obstacles.</td>
|
||||
<td>integer</td>
|
||||
<td>`2`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`AgentHeight`</td>
|
||||
<td>Agent height, in studs. Empty space smaller than this value, like the space under stairs, will be marked as non-traversable.</td>
|
||||
<td>integer</td>
|
||||
<td>`5`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`AgentCanJump`</td>
|
||||
<td>Determines whether jumping during pathfinding is allowed.</td>
|
||||
<td>boolean</td>
|
||||
<td>`true`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`AgentCanClimb`</td>
|
||||
<td>Determines whether climbing `Class.TrussPart|TrussParts` during pathfinding is allowed.</td>
|
||||
<td>boolean</td>
|
||||
<td>`false`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`WaypointSpacing`</td>
|
||||
<td>Spacing between intermediate waypoints in path. If set to `Library.math.huge`, there will be no intermediate waypoints.</td>
|
||||
<td>number</td>
|
||||
<td>`4`</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>`Costs`</td>
|
||||
<td>Table of materials or defined `Class.PathfindingModifier|PathfindingModifiers` and their cost for traversal. Useful for making the agent prefer certain materials/regions over others. See [modifiers](#pathfinding-modifiers) for details.</td>
|
||||
<td>table</td>
|
||||
<td>`nil`</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
```lua title="LocalScript" highlight="3-10"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
|
||||
local path = PathfindingService:CreatePath({
|
||||
AgentRadius = 3,
|
||||
AgentHeight = 6,
|
||||
AgentCanJump = false,
|
||||
Costs = {
|
||||
Water = 20
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Note that the agent can climb `Class.TrussPart|TrussParts` during pathfinding assuming you set `AgentCanClimb` to `true` when [creating the path](#create-paths) and nothing blocks the agent from the truss climbing path. A climbable path has the **Climb** label and the [cost](#set-material-costs) for a climbable path is **1** by default.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/Path-TrussPart.jpg" width="800" alt="Path going up a climbable TrussPart ladder" />
|
||||
|
||||
```lua title="LocalScript - Truss Climbing Path" highlight="6,8"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
|
||||
local path = PathfindingService:CreatePath({
|
||||
AgentCanClimb = true,
|
||||
Costs = {
|
||||
Climb = 2 -- Cost of the climbing path; default is 1
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Move along paths
|
||||
|
||||
This section uses the following pathfinding script for the player's character. To test while reading:
|
||||
|
||||
1. Copy the code into a `Class.LocalScript` within `Class.StarterCharacterScripts`.
|
||||
1. Set the `TEST_DESTINATION` variable to a `Datatype.Vector3` destination in your 3D world that the player character can reach.
|
||||
1. Proceed through the following sections to learn about path computation and character movement.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="11"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local character = player.Character
|
||||
local humanoid = character:WaitForChild("Humanoid")
|
||||
|
||||
local TEST_DESTINATION = Vector3.new(100, 0, 100)
|
||||
|
||||
local waypoints
|
||||
local nextWaypointIndex
|
||||
local reachedConnection
|
||||
local blockedConnection
|
||||
|
||||
local function followPath(destination)
|
||||
-- Compute the path
|
||||
local success, errorMessage = pcall(function()
|
||||
path:ComputeAsync(character.PrimaryPart.Position, destination)
|
||||
end)
|
||||
|
||||
if success and path.Status == Enum.PathStatus.Success then
|
||||
-- Get the path waypoints
|
||||
waypoints = path:GetWaypoints()
|
||||
|
||||
-- Detect if path becomes blocked
|
||||
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
|
||||
-- Check if the obstacle is further down the path
|
||||
if blockedWaypointIndex >= nextWaypointIndex then
|
||||
-- Stop detecting path blockage until path is re-computed
|
||||
blockedConnection:Disconnect()
|
||||
-- Call function to re-compute new path
|
||||
followPath(destination)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Detect when movement to next waypoint is complete
|
||||
if not reachedConnection then
|
||||
reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
|
||||
if reached and nextWaypointIndex < #waypoints then
|
||||
-- Increase waypoint index and move to next waypoint
|
||||
nextWaypointIndex += 1
|
||||
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
|
||||
else
|
||||
reachedConnection:Disconnect()
|
||||
blockedConnection:Disconnect()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Initially move to second waypoint (first waypoint is path start; skip it)
|
||||
nextWaypointIndex = 2
|
||||
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
|
||||
else
|
||||
warn("Path not computed!", errorMessage)
|
||||
end
|
||||
end
|
||||
|
||||
followPath(TEST_DESTINATION)
|
||||
```
|
||||
|
||||
### Compute the path
|
||||
|
||||
After you've created a valid path with `Class.PathfindingService:CreatePath()|CreatePath()`, it must be **computed** by calling `Class.Path:ComputeAsync()` with a `Datatype.Vector3` for both the starting point and destination.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="5, 21"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local character = player.Character
|
||||
local humanoid = character:WaitForChild("Humanoid")
|
||||
|
||||
local TEST_DESTINATION = Vector3.new(100, 0, 100)
|
||||
|
||||
local waypoints
|
||||
local nextWaypointIndex
|
||||
local reachedConnection
|
||||
local blockedConnection
|
||||
|
||||
local function followPath(destination)
|
||||
-- Compute the path
|
||||
local success, errorMessage = pcall(function()
|
||||
path:ComputeAsync(character.PrimaryPart.Position, destination)
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
<img src="../assets/avatar/pathfinding/Path-Start-End.jpg" width="800" alt="Path start/end marked on series of islands and bridges" />
|
||||
|
||||
### Get waypoints
|
||||
|
||||
Once the `Class.Path` is computed, it will contain a series of **waypoints** that trace the path from start to end. These points can be gathered with the `Class.Path:GetWaypoints()` function.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="13, 24, 26"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local character = player.Character
|
||||
local humanoid = character:WaitForChild("Humanoid")
|
||||
|
||||
local TEST_DESTINATION = Vector3.new(100, 0, 100)
|
||||
|
||||
local waypoints
|
||||
local nextWaypointIndex
|
||||
local reachedConnection
|
||||
local blockedConnection
|
||||
|
||||
local function followPath(destination)
|
||||
-- Compute the path
|
||||
local success, errorMessage = pcall(function()
|
||||
path:ComputeAsync(character.PrimaryPart.Position, destination)
|
||||
end)
|
||||
|
||||
if success and path.Status == Enum.PathStatus.Success then
|
||||
-- Get the path waypoints
|
||||
waypoints = path:GetWaypoints()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
<figure>
|
||||
<img src="../assets/avatar/pathfinding/Waypoints.jpg"
|
||||
width="800" alt="Waypoints indicated across computed path" />
|
||||
<figcaption>Waypoints indicated across computed path</figcaption>
|
||||
</figure>
|
||||
|
||||
### Path movement
|
||||
|
||||
Each waypoint consists of both a **position** (`Datatype.Vector3`) and **action** (`Enum.PathWaypointAction|PathWaypointAction`). To move a character containing a `Class.Humanoid`, like a typical Roblox character, the easiest way is to call `Class.Humanoid:MoveTo()` from waypoint to waypoint, using the `Class.Humanoid.MoveToFinished|MoveToFinished` event to detect when the character reaches each waypoint.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="40-51, 54-55"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local character = player.Character
|
||||
local humanoid = character:WaitForChild("Humanoid")
|
||||
|
||||
local TEST_DESTINATION = Vector3.new(100, 0, 100)
|
||||
|
||||
local waypoints
|
||||
local nextWaypointIndex
|
||||
local reachedConnection
|
||||
local blockedConnection
|
||||
|
||||
local function followPath(destination)
|
||||
-- Compute the path
|
||||
local success, errorMessage = pcall(function()
|
||||
path:ComputeAsync(character.PrimaryPart.Position, destination)
|
||||
end)
|
||||
|
||||
if success and path.Status == Enum.PathStatus.Success then
|
||||
-- Get the path waypoints
|
||||
waypoints = path:GetWaypoints()
|
||||
|
||||
-- Detect if path becomes blocked
|
||||
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
|
||||
-- Check if the obstacle is further down the path
|
||||
if blockedWaypointIndex >= nextWaypointIndex then
|
||||
-- Stop detecting path blockage until path is re-computed
|
||||
blockedConnection:Disconnect()
|
||||
-- Call function to re-compute new path
|
||||
followPath(destination)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Detect when movement to next waypoint is complete
|
||||
if not reachedConnection then
|
||||
reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
|
||||
if reached and nextWaypointIndex < #waypoints then
|
||||
-- Increase waypoint index and move to next waypoint
|
||||
nextWaypointIndex += 1
|
||||
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
|
||||
else
|
||||
reachedConnection:Disconnect()
|
||||
blockedConnection:Disconnect()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Initially move to second waypoint (first waypoint is path start; skip it)
|
||||
nextWaypointIndex = 2
|
||||
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
|
||||
else
|
||||
warn("Path not computed!", errorMessage)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
<video controls src="../assets/avatar/pathfinding/Simple-Path.mp4" width="800" alt="Video of character following simple path across islands and bridges"></video>
|
||||
|
||||
### Handle blocked paths
|
||||
|
||||
Many Roblox worlds are dynamic; parts might move or fall and floors may collapse. This can block a computed path and prevent the character from reaching its destination. To handle this, you can connect the `Class.Path.Blocked` event and re-compute the path around whatever blocked it.
|
||||
|
||||
<Alert severity="warning">
|
||||
Paths may also become blocked somewhere **behind** the agent, such as a pile of rubble falling on a path as the agent runs away, but that doesn't mean the agent should stop moving. The <Typography noWrap>`if blockedWaypointIndex >= nextWaypointIndex`</Typography> check makes sure that the path is re-computed only if the blocked waypoint is **ahead** of the current waypoint.
|
||||
</Alert>
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="16, 29-37"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath()
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local character = player.Character
|
||||
local humanoid = character:WaitForChild("Humanoid")
|
||||
|
||||
local TEST_DESTINATION = Vector3.new(100, 0, 100)
|
||||
|
||||
local waypoints
|
||||
local nextWaypointIndex
|
||||
local reachedConnection
|
||||
local blockedConnection
|
||||
|
||||
local function followPath(destination)
|
||||
-- Compute the path
|
||||
local success, errorMessage = pcall(function()
|
||||
path:ComputeAsync(character.PrimaryPart.Position, destination)
|
||||
end)
|
||||
|
||||
if success and path.Status == Enum.PathStatus.Success then
|
||||
-- Get the path waypoints
|
||||
waypoints = path:GetWaypoints()
|
||||
|
||||
-- Detect if path becomes blocked
|
||||
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
|
||||
-- Check if the obstacle is further down the path
|
||||
if blockedWaypointIndex >= nextWaypointIndex then
|
||||
-- Stop detecting path blockage until path is re-computed
|
||||
blockedConnection:Disconnect()
|
||||
-- Call function to re-compute new path
|
||||
followPath(destination)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
<Alert severity="error">
|
||||
Currently, `Class.Model|Models` containing a `Class.Humanoid` instance, including typical player characters, will **not** be considered for path [computation](#compute-the-path) or path blockage, although the agent may still be blocked by those models physically.
|
||||
</Alert>
|
||||
|
||||
## Pathfinding modifiers
|
||||
|
||||
By default, `Class.Path:ComputeAsync()` returns the **shortest** path between the starting point and destination, with the exception that it attempts to avoid jumps. This looks unnatural in some situations — for instance, a path may go through water rather than over a nearby bridge simply because the path through water is geometrically shorter.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/Paths-Shortest-Best.jpg" width="800" alt="Two paths indicated with the shorter path not necessarily more logical" />
|
||||
|
||||
To optimize pathfinding even further, you can implement **pathfinding modifiers** to compute smarter paths across various [materials](#set-material-costs), around defined [regions](#work-with-regions), or through [obstacles](#ignore-obstacles).
|
||||
|
||||
### Set material costs
|
||||
|
||||
When working with `Class.Terrain` and `Class.BasePart` materials, you can include a `Costs` table within `Class.PathfindingService:CreatePath()|CreatePath()` to make certain materials more traversable than others. All materials have a default cost of **1** and any material can be defined as non-traversable by setting its value to `Library.math.huge`.
|
||||
|
||||
Keys in the `Costs` table should be string names representing `Enum.Material` names, for example `Water` for `Enum.Material.Water`.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="6-10"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath({
|
||||
Costs = {
|
||||
Water = 20,
|
||||
Mud = 5,
|
||||
Neon = math.huge
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
<video controls src="../assets/avatar/pathfinding/Bridge-Path.mp4" width="800" alt="Video showing how material costs determine preference of non-water traversal"></video>
|
||||
|
||||
### Work with regions
|
||||
|
||||
In some cases, [material preference](#set-material-costs) is not enough. For example, you might want characters to avoid a **defined region**, regardless of the materials underfoot. This can be achieved by adding a `Class.PathfindingModifier` object to a part.
|
||||
|
||||
1. Create an `Class.BasePart.Anchored|Anchored` part around the dangerous region and set its `Class.BasePart.CanCollide|CanCollide` property to **false**.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/GeyserBlocker-Block.jpg" width="750" alt="Anchored part defining a region to apply a pathfinding modifier to" />
|
||||
|
||||
1. Insert a `Class.PathfindingModifier` instance onto the part, locate its `Class.PathfindingModifier.Label|Label` property, and assign a meaningful name like **DangerZone**.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/GeyserBlocker-PathfindingModifier-Label.png" width="320" alt="PathfindingModifier instance with Label property set to DangerZone" />
|
||||
|
||||
1. Include a `Costs` table within `Class.PathfindingService:CreatePath()|CreatePath()` containing a matching key and associated numeric value. A modifier can be defined as non-traversable by setting its value to `Library.math.huge`.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="6-8"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath({
|
||||
Costs = {
|
||||
DangerZone = math.huge
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
<video controls src="../assets/avatar/pathfinding/GeyserBlocker-Path.mp4" width="800"></video>
|
||||
|
||||
### Ignore obstacles
|
||||
|
||||
In some cases, it's useful to pathfind through solid obstacles as if they didn't exist. This lets you compute a path through specific physical blockers, versus the computation failing outright.
|
||||
|
||||
1. Create an `Class.BasePart.Anchored|Anchored` part around the object and set its `Class.BasePart.CanCollide|CanCollide` property to **false**.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/DoorPassThrough-Block.jpg" width="750" alt="Anchored part defining a region to apply a pathfinding modifier to" />
|
||||
|
||||
1. Insert a `Class.PathfindingModifier` instance onto the part and enable its `Class.PathfindingModifier.PassThrough|PassThrough` property.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/DoorPassThrough-PathfindingModifier-PassThrough.png" width="320" alt="PathfindingModifier instance with PassThrough property enabled" />
|
||||
|
||||
Now, when a path is computed from the zombie NPC to the player character, the path extends beyond the door and you can prompt the zombie to traverse it. Even if the zombie is unable to open the door, it reacts as if it "hears" the character behind the door.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/Zombie-Full-Path.jpg" width="750" alt="Zombie NPC path passing through the previously blocking door" />
|
||||
|
||||
## Pathfinding links
|
||||
|
||||
Sometimes it's necessary to find a path across a space that cannot be normally traversed, such as across a chasm, and perform a custom action to reach the next waypoint. This can be achieved through the `Class.PathfindingLink` object.
|
||||
|
||||
Using the island example from above, you can make the agent use a boat instead of walking across all of the bridges.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/PathfindingLink-Path.jpg" width="800" alt="PathfindingLink showing how an agent can use a boat instead of walking across all of the bridges" />
|
||||
|
||||
To create a `Class.PathfindingLink` using this example:
|
||||
|
||||
1. <Chip label="optional" size="small" variant="outlined" color="primary" /> To assist with visualization and debugging, toggle on **Pathfinding links** from the [Visualization Options](../studio/ui-overview.md#visualization-options) widget in the upper‑right corner of the 3D viewport.
|
||||
1. Create two `Class.Attachment|Attachments`, one on the boat's seat and one near the boat's landing point.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/PathfindingLink-Attachments.jpg" width="750" alt="Attachments created for pathfinding link's start and end" />
|
||||
|
||||
1. Create a `Class.PathfindingLink` object in the workspace, then assign its **Attachment0** and **Attachment1** properties to the starting and ending attachments respectively.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/PathfindingLink-Attachments-Properties.png" width="320" alt="Attachment0/Attachment1 properties of a PathfindingLink" />
|
||||
|
||||
<img src="../assets/avatar/pathfinding/PathfindingLink-In-World.jpg" width="750" alt="PathfindingLink visualized in the 3D world" />
|
||||
|
||||
1. Assign a meaningful name like **UseBoat** to its `Class.PathfindingLink.Label|Label` property. This name is used as a flag in the pathfinding script to trigger a custom action when the agent reaches the starting link point.
|
||||
|
||||
<img src="../assets/avatar/pathfinding/PathfindingLink-Label.png" width="320" alt="Label property specified for PathfindingLink" />
|
||||
|
||||
1. Include a `Costs` table within `Class.PathfindingService:CreatePath()|CreatePath()` containing both a `Water` key and a custom key matching the `Class.PathfindingLink.Label|Label` property name. Assign the custom key a lower value than `Water`.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="6-9"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath({
|
||||
Costs = {
|
||||
Water = 20,
|
||||
UseBoat = 1
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
1. In the event which fires when a waypoint is reached, add a custom check for the `Class.PathfindingLink.Label|Label` modifier name and take a different action than `Class.Humanoid:MoveTo()` — in this case, calling a function to seat the agent in the boat, move the boat across the water, and continue the agent's path upon arrival at the destination island.
|
||||
|
||||
```lua title="LocalScript - Character Pathfinding" highlight="52-56, 72"
|
||||
local PathfindingService = game:GetService("PathfindingService")
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Workspace = game:GetService("Workspace")
|
||||
|
||||
local path = PathfindingService:CreatePath({
|
||||
Costs = {
|
||||
Water = 20,
|
||||
UseBoat = 1
|
||||
}
|
||||
})
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local character = player.Character
|
||||
local humanoid = character:WaitForChild("Humanoid")
|
||||
|
||||
local TEST_DESTINATION = Vector3.new(228.9, 17.8, 292.5)
|
||||
|
||||
local waypoints
|
||||
local nextWaypointIndex
|
||||
local reachedConnection
|
||||
local blockedConnection
|
||||
|
||||
local function followPath(destination)
|
||||
-- Compute the path
|
||||
local success, errorMessage = pcall(function()
|
||||
path:ComputeAsync(character.PrimaryPart.Position, destination)
|
||||
end)
|
||||
|
||||
if success and path.Status == Enum.PathStatus.Success then
|
||||
-- Get the path waypoints
|
||||
waypoints = path:GetWaypoints()
|
||||
|
||||
-- Detect if path becomes blocked
|
||||
blockedConnection = path.Blocked:Connect(function(blockedWaypointIndex)
|
||||
-- Check if the obstacle is further down the path
|
||||
if blockedWaypointIndex >= nextWaypointIndex then
|
||||
-- Stop detecting path blockage until path is re-computed
|
||||
blockedConnection:Disconnect()
|
||||
-- Call function to re-compute new path
|
||||
followPath(destination)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Detect when movement to next waypoint is complete
|
||||
if not reachedConnection then
|
||||
reachedConnection = humanoid.MoveToFinished:Connect(function(reached)
|
||||
if reached and nextWaypointIndex < #waypoints then
|
||||
-- Increase waypoint index and move to next waypoint
|
||||
nextWaypointIndex += 1
|
||||
|
||||
-- Use boat if waypoint label is "UseBoat"; otherwise move to next waypoint
|
||||
if waypoints[nextWaypointIndex].Label == "UseBoat" then
|
||||
useBoat()
|
||||
else
|
||||
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
|
||||
end
|
||||
else
|
||||
reachedConnection:Disconnect()
|
||||
blockedConnection:Disconnect()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Initially move to second waypoint (first waypoint is path start; skip it)
|
||||
nextWaypointIndex = 2
|
||||
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
|
||||
else
|
||||
warn("Path not computed!", errorMessage)
|
||||
end
|
||||
end
|
||||
|
||||
function useBoat()
|
||||
local boat = Workspace.BoatModel
|
||||
|
||||
humanoid.Seated:Connect(function()
|
||||
-- Start boat moving if agent is seated
|
||||
if humanoid.Sit then
|
||||
task.wait(1)
|
||||
boat.CylindricalConstraint.Velocity = 5
|
||||
end
|
||||
-- Detect constraint position in relation to island
|
||||
local boatPositionConnection
|
||||
boatPositionConnection = RunService.PostSimulation:Connect(function()
|
||||
-- Stop boat when next to island
|
||||
if boat.CylindricalConstraint.CurrentPosition >= 94 then
|
||||
boatPositionConnection:Disconnect()
|
||||
boat.CylindricalConstraint.Velocity = 0
|
||||
task.wait(1)
|
||||
-- Unseat agent and continue to destination
|
||||
humanoid.Sit = false
|
||||
humanoid:MoveTo(waypoints[nextWaypointIndex].Position)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
followPath(TEST_DESTINATION)
|
||||
```
|
||||
|
||||
<video controls src="../assets/avatar/pathfinding/Boat-Path.mp4" width="800" alt="Video showing character using the PathfindingLink to traverse the water using the boat" ></video>
|
||||
|
||||
## Streaming compatibility
|
||||
|
||||
In-experience [instance streaming](../workspace/streaming.md) is a powerful feature that dynamically loads and unloads 3D content as a player's character moves around the world. As they explore the 3D space, new subsets of the space stream to their device and some of the existing subsets might stream out.
|
||||
|
||||
Consider the following best practices for using `Class.PathfindingService` in streaming-enabled experiences:
|
||||
|
||||
- Streaming can block or unblock a given path as a character moves along it. For example, while a character runs through a forest, a tree might stream in somewhere ahead of them and obstruct the path. To make pathfinding work seamlessly with streaming, it's highly recommended that you use the [handling blocked paths](#handle-blocked-paths) technique and re-compute the path when necessary.
|
||||
|
||||
- A common approach in pathfinding is to use the coordinates of existing objects for [computation](#compute-the-path), such as setting a path destination to the position of an existing **TreasureChest** model in the world. This approach is fully compatible with server-side `Class.Script|Scripts` since the server has full view of the world at all times, but `Class.LocalScript|LocalScripts` and `Class.ModuleScript|ModuleScripts` that run on the client may fail if they attempt to compute a path to an object that's not streamed in.
|
||||
|
||||
To address this issue, consider setting the destination to the position of a `Class.BasePart` within a [persistent](../workspace/streaming.md#model-streaming-controls) model. Persistent models load soon after the player joins and they never stream out, so a client-side script can connect to the `Class.Workspace.PersistentLoaded|PersistentLoaded` event and safely access the model for creating waypoints after the event fires.
|
||||
75
.cursor/rules/roblox/en-us/characters/r6-to-r15-adapter.md
Normal file
75
.cursor/rules/roblox/en-us/characters/r6-to-r15-adapter.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
title: R6 to R15 Adapter
|
||||
description: The R6 to R15 adapter enables R15 avatar characters to join your R6 experience.
|
||||
---
|
||||
|
||||
<Alert severity = 'warning'>
|
||||
This feature is only applicable to developers who manage an active experience with **R6** avatar characters enabled.
|
||||
</Alert>
|
||||
|
||||
The **R6 to R15 Adapter** allows R15 avatars to join your R6 experience. All avatars in the experience will still use the R6-like scale and movement systems. The adapter allows your experience to take advantage of modern R15 components, such as layered clothing and animatable heads, with minimal performance or gameplay impact to your experience.
|
||||
|
||||
It's important to understand how the adapter uses [adapter parts](#adapter-parts) and review the feature's [known limitations](#known-limitations) before [enabling](#enable-the-r6-to-r15-adapter) and testing the adapter for your experience.
|
||||
|
||||
## Adapter parts
|
||||
|
||||
The R6 to R15 Adapter implements a Luau script injection when an avatar spawns that creates adapter parts.
|
||||
|
||||
These are invisible `Class.MeshPart|MeshParts` that have the exact same name as R6 body parts and are welded to their corresponding R15 body parts. The scripts allow adapter parts to accept R6-based script interactions and forward them to the appropriate R15 parts.
|
||||
|
||||
The adapter parts perform the following:
|
||||
|
||||
- Emulates R6 physics using extra invisible collideable parts with the positions and dimensions of R6 hitboxes.
|
||||
- Sets the visible R15 body parts as non-collideable.
|
||||
- Scales and positions the visible R15 parts to match the R6 size and joint positions.
|
||||
- Acts as a shim between R6 and R15 body parts. Property changes applied to the invisible R6 parts are passed along to their corresponding visible R15 parts.
|
||||
- For example, a color change in R6 `LeftArm` is passed to the R15 `LeftUpperArm`, `LeftLowerArm` and `LeftHand` parts.
|
||||
|
||||
## Enable the R6 to R15 Adapter
|
||||
|
||||
You can enable the R6 to R15 Adapter by setting the `Class.Workspace.AvatarUnificationMode|AvatarUnificationMode` property in `Class.Workspace`. You can only access this property if **Avatar Type** is set to `R6` in Studio's **File** ⟩ **Experience Settings** window. At this time, the **Default** setting disables unification mode.
|
||||
|
||||
To enable the R6 to R15 Adapter:
|
||||
|
||||
1. In the **Explorer**, navigate to **Workspace**.
|
||||
2. In the **Properties** window, set **AvatarUnificationMode** to **Enabled**.
|
||||
|
||||
## Known limitations
|
||||
|
||||
In a majority of cases, the R6 to R15 Adapter works out-of-the-box with the systems of a R6 experience. In rare cases, there may be conflicts with custom systems that handle security, or character-related behavior. See the following for a list of potential limitations or conflicts that you may encounter when using the R6 to R15 Adapter.
|
||||
|
||||
### Security
|
||||
|
||||
Some R6 experiences with active cheat detection can interpret the Luau script injection as an attempt to circumvent the security.
|
||||
|
||||
Since the default behavior of R6 avatars is to spawn with all their parts already in place, many experiences tend to flag changes in body parts as potential exploits. In experiences with the adapter enabled, the R15 avatars spawn with their default bodies before changing body parts based on their saved avatar body parts and accessories.
|
||||
|
||||
### Custom avatar editors
|
||||
|
||||
Experiences with custom avatar editors that allow players to swap body parts might break the connection with the adapter parts.
|
||||
|
||||
### Pre-existing R15 support
|
||||
|
||||
Experiences that check the avatar rig type and include solutions specific for each R15 and R6 case might not work properly with the adapter. `AvatarUnificationMode` uses code paths corresponding to R15 which may require testing in your experience.
|
||||
|
||||
### Body part rescaling
|
||||
|
||||
Experiences that resize the R6 body parts won't see the scale change propagated to the proxied R15 parts. This is also the case if joint attachments are moved.
|
||||
|
||||
### GetChildren API calls
|
||||
|
||||
`Class.Instance:GetChildren()|GetChildren()` calls return both the R6 proxy parts and their corresponding R15 parts. You may need to account for this extra information.
|
||||
|
||||
### FindFirstChild API calls
|
||||
|
||||
Don't immediately use `Class.Instance:FindFirstChild()|FindFirstChild()`, or "dot indexing," in your scripts to find character parts. Instead, use `Class.Instance:WaitForChild()|WaitForChild()` before you call `Class.Instance:FindFirstChild()|FindFirstChild()`. Replication in `Class.Workspace.AvatarUnificationMode|AvatarUnificationMode` is different, and the experience may fail to find a child that doesn't exist yet.
|
||||
|
||||
This has always been a best practice for Roblox scripts, even if some cases work without following this practice.
|
||||
|
||||
### Head.ClassName conditionals
|
||||
|
||||
`Class.Workspace.AvatarUnificationMode|AvatarUnificationMode` sets the Head to a `Class.MeshPart`. Any calls that read or write to the `Class.SpecialMesh.MeshId` property will fail.
|
||||
|
||||
### Head collisions
|
||||
|
||||
R15 characters joining an R6 experience don't support collisions with their Head part. If your experience detects or depends on collisions with a character's Head, you must also update your scripts to check for `CollisionHead` as well.
|
||||
Loading…
Add table
Add a link
Reference in a new issue