플레이어가 실시간으로 아바타 바디를 생성하고 사용자화하며 구매할 수 있는 경험을 게시할 수 있습니다. 구매 시, 이러한 사용자화된 바디는 플레이어의 Roblox 인벤토리에 직접 저장되어, 다른 경험에서도 커스터마이즈된 아바타를 장착하고 착용할 수 있습니다.
경험 소유자가 경험 내 아바타 만들기를 구현하면 아바타 아이템의 제작자와 경험 소유자로서 Marketplace 수수료 혜택을 누릴 수 있습니다. 경험 내에서 생성된 자산이 검사될 경우, 해당 항목은 생성된 원래 경험으로의 링크를 제공합니다.
경험 내 생성을 테스트하려면 Roblox의 Avatar Creator 데모를 사용하십시오.
경험 내 생성을 구현하는 방법
다음 지침 및 코드 참조를 사용하여 첫 번째 경험 내 아바타 생성 프로젝트를 만듭니다. 다음 지침에서는 플레이어가 게시하기 전에 수정하고 사용자화할 수 있는 기본 바디 Model을 사용합니다.
시작하기 전에 다음 내용에 익숙해지십시오:
- 아바타 모델 — 다음 구현은 Roblox의 15부품 사양을 충족하는 기본 바디를 가져와야 합니다. 이 Model은 추가 사용자 커스터마이징 및 수정을 위한 기본 역할을 합니다.
- 기본 바디는 Roblox의 아바타 바디 가이드라인을 따라야 하며, 얼굴 리깅을 위한 최소한의 FACS 제어를 포함해야 합니다.
- 아바타 생성 토큰 — 아바타 생성을 구현하는 경험은 최소한 하나의 생성 토큰이 필요합니다. 이 토큰은 Robux로 구매해야 하며, 경험 내에서 이루어지는 구매에 대한 가격 및 기타 판매 설정을 설정할 수 있습니다.
- API 클래스
- AvatarCreationService — 아바타 생성 요청 및 검증을 처리합니다.
- EditableImage — 텍스처의 런타임 생성 및 조작을 처리합니다.
- EditableMesh — 메시 기하학의 런타임 조작을 처리합니다.
- WrapDeformer — 아바타 캐릭터가 3D 의류를 장착할 수 있도록 하는 겉면의 보이지 않는 외부 케이지 기하학의 런타임 조작을 처리합니다.
기본 바디 가져오기
기본 바디는 사용자가 사용자화하고 편집할 수 있는 초기 기초 역할을 합니다. 자신의 Model을 사용하거나 Importer를 사용하여 사용자 자산을 가져오고 Avatar Setup을 통해 설정할 수 있습니다.
기본 바디는 Roblox의 아바타 사양을 준수해야 하며, 6개의 신체 부위를 구성하는 15개의 MeshPart 인스턴스와 같은 구성 요소 및 기타 아바타 구성 요소를 포함해야 합니다.
적절하게 구성된 아바타 바디의 참조 및 샘플은 아바타 참조를 참조하십시오.
편집 API 구현하기
사용자가 경험 내에서 아바타의 MeshPart 인스턴스를 편집할 수 있는 시스템을 개발하기 위해, 텍스처 편집을 위해 EditableImage, 메시 편집을 위해 EditableMesh, 메시 편집 중 스키닝과 FACS 데이터를 유지하기 위해 WrapDeformer를 사용하세요.
기본 바디를 가져온 후 다음 스크립트를 사용하여 EditableImages, EditableMeshes, 및 WrapDeformers를 설정합니다.
local AssetService = game:GetService("AssetService")local function setupBodyPart(meshPart, wrapTarget)-- MeshPart에 WrapDeformer 생성 및 연결local wrapDeformer = Instance.new("WrapDeformer")wrapDeformer.Parent = meshPart-- 래핑 대상의 케이지 메시를 위한 편집 가능한 메시 생성local cageEditableMesh: EditableMesh =AssetService:CreateEditableMeshAsync(Content.fromUri(wrapTarget.CageMeshId), {FixedSize = true,})-- WrapDeformer에 케이지 메시 할당wrapDeformer:SetCageMeshContent(Content.fromObject(cageEditableMesh))endlocal function setupRigidMesh(meshPart)-- 원래 MeshPart에서 편집 가능한 메시 생성local editableMesh = AssetService:CreateEditableMeshAsync(Content.fromUri(meshPart.MeshId), {FixedSize = true,})-- 편집 가능한 메시로부터 새 MeshPart 생성local newMeshPart = AssetService:CreateMeshPartAsync(Content.fromObject(editableMesh))-- 원래 MeshPart의 크기, 위치 및 텍스처 복사newMeshPart.Size = meshPart.SizenewMeshPart.CFrame = meshPart.CFramenewMeshPart.TextureContent = meshPart.TextureContent-- 원래 MeshPart에 새 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 자식을 추가-- 그렇지 않으면 바로 MeshPart에 EditableMesh 적용local wrapTarget = descendant:FindFirstChildOfClass("WrapTarget")if wrapTarget thensetupBodyPart(descendant, wrapTarget)elsesetupRigidMesh(descendant)end-- MeshPart에 대해 EditableImage 구성setupMeshTexture(descendant, textureIdToEditableImageMap)endend플레이어가 기본 바디에 색상을 변경하거나 그림을 그리고 스티커를 추가할 수 있도록 하는 EditableImage 도구를 생성합니다. DrawImage(), DrawRectangle(), WritePixelsBuffer()와 같은 API를 활용할 수 있습니다.
고급 변환을 위해 DrawImageTransformed()를 사용하면 한 EditableImage를 다른 EditableImage에 그릴 때 위치, 회전 및 스케일을 지정할 수 있습니다. 마찬가지로, DrawImageProjected()는 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 신체 부위 각각은 다음을 포함해야 합니다:
제공된 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 매개변수를 사용합니다. 이 토큰은 귀하의 유니버스에서 생성에 대한 키이며, 귀하의 경험에서 아바타 생성의 가격을 설정하는 데 사용됩니다. 토큰 생성에 관한 지침 및 추가 세부정보는 아바타 생성 토큰을 참조하십시오.
토큰을 구매하고 생성한 후에는 Creator Hub에서 토큰을 검사하여 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