您可以发布一种体验,允许玩家实时创建、自定义和购买头像身体。当购买后,这些自定义身体会直接保存到玩家的 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() 的接口。
对于高级变换,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 处理渲染的 MeshPart 几何的实时变形,同时维护底层的皮肤和 FACS 数据。
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
end
AvatarCreationService: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 参数。此令牌是您宇宙创建的关键,您可以使用它来设置来自体验的头像创建价格。有关生成令牌的说明和更多详细信息,请参阅 头像创建令牌。
在购买和生成您的令牌后,您可以在创作者中心检查该令牌以找到随后可用于 AvatarCreationService:PromptCreateAvatarAsync() API 的 ID。

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

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