您可以发布一个允许玩家实时创建、定制和购买头像模型的体验。当购买后,这些自定义身体将直接保存到玩家的 Roblox 库中,使玩家能够在其他体验中装备和穿戴这些自定义头像。
实施体验内头像创建的体验拥有者可从 市场佣金 中受益,作为头像物品的 创作者 和 体验拥有者。如果在体验中创建的资产被检查,则该物品提供指向其创建原始体验的链接。
您可以在 Roblox 的 头像创建者 演示中测试体验内创建。
如何实现体验内创建
使用以下说明和代码参考来创建您的第一个体验内头像创建项目。以下说明使用基础身体 Model,玩家可以在发布之前进行修改和自定义。
在开始之前,请熟悉以下内容:
- 头像创建令牌 — 实施头像创建的体验至少需要一个创建令牌。这些令牌需要 Robux 来购买,并允许您设置以体验内购买为基础的价格和其他销售设置。
- API 类
- AvatarCreationService — 处理头像创建提示和验证。
- EditableImage — 处理纹理的运行时创建和操控。
- EditableMesh — 处理网格几何的运行时操控。
- WrapDeformer — 处理运行时操控的不可见外部笼子几何,使头像角色能够装备 3D 服装。
导入基础身体
基础身体作为用户可以自定义和编辑的初始基础。您可以使用自己的 Model,或通过 导入器 导入自定义资产并通过 头像设置 进行设置。
基础身体必须遵循 Roblox 的头像规格,并必须包括如 15 个 MeshPart 实例构成的 6 个身体部位:头、躯干、左臂、左腿、右臂和右腿,以及其他 头像组件。
有关正确配置的头像身体的参考和示例,请参见 头像参考。
实现编辑 API
要开发一个系统,让用户能够在您的体验中编辑头像的 MeshPart 实例以进行创建,请使用 EditableImage 进行纹理编辑,使用 EditableMesh 进行网格编辑,并使用 WrapDeformer 在网格编辑时维护蒙皮和 FACS 数据。
导入基础身体后,使用以下脚本设置您的 EditableImages、EditableMeshes 和 WrapDeformers。
local AssetService = game:GetService("AssetService")local function setupBodyPart(meshPart, wrapTarget)-- 创建并将 WrapDeformer 附加到 MeshPartlocal wrapDeformer = Instance.new("WrapDeformer")wrapDeformer.Parent = meshPart-- 为包裹目标的笼子网格创建可编辑网格local cageEditableMesh: EditableMesh =AssetService:CreateEditableMeshAsync(Content.fromUri(wrapTarget.CageMeshId), {FixedSize = true,})-- 将笼子网格分配给 WrapDeformerwrapDeformer:SetCageMeshContent(Content.fromObject(cageEditableMesh))endlocal function setupRigidMesh(meshPart)-- 从原始 MeshPart 创建可编辑网格local editableMesh = AssetService:CreateEditableMeshAsync(Content.fromUri(meshPart.MeshId), {FixedSize = true,})-- 从可编辑网格生成新的 MeshPartlocal newMeshPart = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))-- 从原始 MeshPart 复制大小、位置和纹理newMeshPart.Size = meshPart.SizenewMeshPart.CFrame = meshPart.CFramenewMeshPart.TextureContent = meshPart.TextureContent-- 将新 MeshPart 应用回原始meshPart:ApplyMesh(newMeshPart)endlocal function setupMeshTexture(meshPart, textureIdToEditableImageMap)-- 如果已经存在此 TextureID 的 EditableImage,则重用它而不是创建新的if textureIdToEditableImageMap[meshPart.TextureID] thenmeshPart.TextureContent =Content.fromObject(textureIdToEditableImageMap[meshPart.TextureID])returnend-- 创建一个新的 EditableImage 并将其应用为纹理内容local editableImage = AssetService:CreateEditableImageAsync(Content.fromUri(meshPart.TextureID))textureIdToEditableImageMap[meshPart.TextureID] = editableImagemeshPart.TextureContent = Content.fromObject(editableImage)endlocal function setupModel(model)-- 按纹理 ID 重用 EditableImage 实例的映射local textureIdToEditableImageMap = {}for _, descendant in model:GetDescendants() doif not descendant:IsA("MeshPart") thencontinueend-- 根据是否存在 WrapTarget 配置 MeshPart-- 如果存在 WrapTarget,则添加一个带有 EditableMesh 的 WrapDeformer 子项-- 否则,将 EditableMesh 直接应用于 MeshPartlocal wrapTarget = descendant:FindFirstChildOfClass("WrapTarget")if wrapTarget thensetupBodyPart(descendant, wrapTarget)elsesetupRigidMesh(descendant)end-- 为 MeshPart 配置 EditableImagesetupMeshTexture(descendant, textureIdToEditableImageMap)endend创建 EditableImage 工具,允许玩家重新上色、绘制或在您的基础身体上添加贴纸。您可以利用如 DrawImage()、DrawRectangle()、WritePixelsBuffer() 的 API。
对于高级变换,DrawImageTransformed() 允许您在绘制一个 EditableImage 到另一个时指定位置、旋转和缩放。同样,DrawImageProjected() 的工作方式与 DrawImage() 类似,但如果 EditableImage 实例与 MeshPart 一起使用,则正确投影绘制的图像。
local function recolorTexture(meshPart: MeshPart,color: Color3)local bodyPartTexture = AssetService:CreateEditableImageAsync(meshPart.TextureID)meshPart.TextureContent = Content.fromObject(bodyPartTexture)bodyPartTexture:DrawRectangle(Vector2.new(0, 0),bodyPartTexture.Size,color,0,Enum.ImageCombineType.Overwrite)endlocal function applySticker(meshPart: MeshPart,textureCoordinate: Vector2,stickerId: TextureId)local bodyPartTexture = AssetService:CreateEditableImageAsync(meshPart.TextureID)meshPart.TextureContent = Content.fromObject(bodyPartTexture)local stickerTexture = AssetService:CreateEditableImageAsync(stickerId)bodyPartTexture:DrawImage(textureCoordinate, stickerTexture, Enum.ImageCombineType.BlendSourceOver)endlocal function applyStickerProjected(meshPart: MeshPart,targetMesh: EditableMesh,stickerId: TextureId,raycastHitPos: Vector3)local bodyPartTexture = AssetService:CreateEditableImageAsync(meshPart.TextureID)local relativePos = meshPart.CFrame:PointToWorldSpace(raycastHitPos)local direction = (workspace.CurrentCamera.CFrame.Position - relativePos).Unitlocal projectionParams: ProjectionParams = {Direction = meshPart.CFrame:VectorToObjectSpace(direction),Position = meshPart.CFrame:PointToObjectSpace(relativePos),Size = Vector3.new(1, 1, 1),Up = meshPart.CFrame:VectorToObjectSpace(Vector3.new(0, 1, 0)),}local stickerTexture = AssetService:CreateEditableImageAsync(stickerId)local localBrushConfig: BrushConfig = {Decal = stickerTexture,ColorBlendType = Enum.ImageCombineType.BlendSourceOver,AlphaBlendType = Enum.ImageAlphaType.Default,BlendIntensity = 1,FadeAngle = 90.0}bodyPartTexture:DrawImageProjected(targetMesh, projectionParams, localBrushConfig)end
使用 WrapDeformer 和 EditableMesh,创建用于编辑您身体的网格变形的工具。
WrapDeformer 在保持底层蒙皮和 FACS 数据的同时处理渲染的 MeshPart 几何的实时变形。
EditableMesh 允许您修改 WrapDeformer 响应的笼子网格。
local function deformBodyPart(meshPart: MeshPart,controlPointCenter: Vector3,controlPointRadius: number,controlPointDeformation: Vector3)local wrapTarget = meshPart:FindFirstChildWhichIsA("WrapTarget")local cageMeshId = wrapTarget.CageMeshIdlocal wrapDeformer = Instance.new("WrapDeformer")wrapDeformer.Parent = meshPartlocal cageEditableMesh = AssetService:CreateEditableMeshAsync(cageMeshId)local verticesWithinSphere =cageEditableMesh:FindVerticesWithinSphere(controlPointCenter, controlPointRadius)for _, vertexId in verticesWithinSphere dolocal vertexPosition = cageEditableMesh:GetPosition(vertexId)cageEditableMesh:SetPosition(vertexId, vertexPosition + controlPointDeformation)endwrapDeformer:SetCageMeshContent(Content.fromObject(cageEditableMesh))end
创建创建提示
在设置好基础身体和编辑 API 后,创建一个提示,提示用户使用 AvatarCreationService:PromptCreateAvatarAsync() 从体验中创建和购买。
export type BodyPartInfo = {
bodyPart: Enum.BodyPart,
instance: Instance --包含已创建 MeshParts 的文件夹
}
export type BodyPartList = {BodyPartInfo}
local function publishAvatar(bodyPartInstances: BodyPartList, player: Player, tokenId: string)
local humanoidDescription = Instance.new("HumanoidDescription")
for _, bodyPartInfo in bodyPartInstances do
local bodyPartDescription = Instance.new("BodyPartDescription")
bodyPartDescription.Instance = bodyPartInfo.instance
bodyPartDescription.BodyPart = bodyPartInfo.bodyPart
bodyPartDescription.Parent = humanoidDescription
end
local success, result, bundleIdOrErrorMessage, outfitId = pcall(function()
return AvatarCreationService:PromptCreateAvatarAsync(tokenId, player, humanoidDescription)
end)
if success then
if result == Enum.PromptCreateAvatarResult.Success then
print("成功上传,BundleId: ", bundleIdOrErrorMessage)
print("成功上传,OutfitId: ", outfitId)
else
print("上传失败,错误信息:", bundleIdOrErrorMessage)
end
else
print("头像创建失败")
end
endAvatarCreationService:PromptCreateAvatarAsync() 需要一个 HumanoidDescription 参数来表示要购买或创建的头像。对于头像创建,角色的 HumanoidDescription 必须包括每个 6 个身体部位的新资产 (Head, Torso, RightLeg, LeftLeg, RightArm, LeftArm)。可选地,它还可以包括一个新的 Hair 配件。
为此,HumanoidDescription 应包含 6 个 BodyPartDescription 子项。每个 BodyPartDescription.Instance 属性引用一个包含构成身体部位的所有 MeshPart 实例的 Folder。例如,LeftArm 文件夹包含 LeftHand、LeftUpperArm 和 LeftLowerArm MeshParts。 BodyPartDescription.BodyPart 属性也应设置为相关的 Enum.BodyPart。
每个 15 个 MeshPart 身体部位必须包括:
- 一个 EditableImage。
- 一个带有 EditableMesh 的 WrapDeformer。
提供的 HumanoidDescription 不应包含任何预先存在的资产 ID 来表示预期创建的身体部位或配件。另一方面,HumanoidDescription 可以包括 BodyTypeScale、HeadScale、HeightScale、WidthScale 和 ProportionScale 的人形比例。注意基础身体的导入比例,以便与提供给 HumanoidDescription 的比例匹配。

包括配件
如果包括配件,例如头发,则 HumanoidDescription 应包括一个子 AccessoryDescription,其中:
- AccessoryDescription.Instance 属性引用 Accessory 实例。
- AccessoryDescription.AccessoryType 属性设置为相关的 Enum.AccessoryType。
- 在您的创作中包含 Enum.AccessoryType.Hair 的情况下,MeshPart 应包含一个 EditableImage。但是,它不应包含 WrapDeformer 子项,而应直接在 MeshPart 上设置一个 EditableMesh。
生成头像创建令牌
AvatarCreationService:PromptCreateAvatarAsync() 需要一个 头像创建令牌 ID 参数。此令牌是您宇宙创作的关键,您可以使用它来设置体验中头像创建的价格。有关生成令牌的说明和其他详细信息,请参见 头像创建令牌。
购买并生成您的令牌后,您可以在创作者中心检查此令牌以找到 ID,然后您可以用于 AvatarCreationService:PromptCreateAvatarAsync() API。

通过归属响应玩家加入
在体验中创建的头像捆绑包包括指向其创建原始体验的归属链接。如果另一个玩家检查该头像,则会显示一个提示,提供访问创建该头像的体验的选项。

要处理使用此归属链接加入您体验的玩家,请使用 Player:GetJoinData() 并解析返回的表以获取 GameJoinContext。
GameJoinContext 包括以下表值:
- JoinSource — Enum.JoinSource
- 从此归属链接加入您的体验的 Player 将具有 Enum.JoinSource.CreatedItemAttribution,以指示从已创建物品入口。
- ItemType — 可选 Enum.AvatarItemType
- AssetId — 可选 string
- OutfitId — 可选 string
- AssetType — 可选 Enum.AssetType