I’ve been working on a custom character controller with it’s own replication system, each client has his own copy of all player’s avatars and moves them according to state data comming from the server.
I need an efficient and low-overhead way to get the character model
here is my current implementation:
local InsertService = game:GetService("InsertService")
local PlayersService = game:GetService("Players")
local RepStorage = game:GetService("ReplicatedStorage")
local GetPlayerModelRequest = RepStorage.GetPlayerModelRequest
local DefaultRig = script.DefaultRig
local function onGetPlayerModelRequest(_, playerId: number): Model
assert(typeof(playerId) == "number", "Player Id must be a number")
local avatarData = PlayersService:GetCharacterAppearanceInfoAsync(playerId)
local character = DefaultRig:Clone()
character.Name = PlayersService:GetNameFromUserIdAsync(playerId)
-- Apply Body Colors
local bodyColors = Instance.new("BodyColors")
bodyColors.HeadColor = BrickColor.new(avatarData.bodyColors.headColorId)
bodyColors.TorsoColor = BrickColor.new(avatarData.bodyColors.torsoColorId)
bodyColors.LeftArmColor = BrickColor.new(avatarData.bodyColors.leftArmColorId)
bodyColors.RightArmColor = BrickColor.new(avatarData.bodyColors.rightArmColorId)
bodyColors.LeftLegColor = BrickColor.new(avatarData.bodyColors.leftLegColorId)
bodyColors.RightLegColor = BrickColor.new(avatarData.bodyColors.rightLegColorId)
bodyColors.Parent = character
-- Load accessories, clothing, and body parts from assets
for _, asset in ipairs(avatarData.assets) do
local assetModel = InsertService:LoadAsset(asset.id)
for _, loadedAsset in ipairs(assetModel:GetChildren()) do
for _, part: Part in loadedAsset:GetDescendants() do
if part:IsA("Part") then
part.CastShadow = false
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
end
end
loadedAsset.Parent = character -- Assuming `character` is your player's character model
end
end
-- Return the constructed character model
character.Parent = workspace -- issue here !
return character
end
GetPlayerModelRequest.OnServerInvoke = onGetPlayerModelRequest
The main issue with this approach is that I have to set the parent of this model as you can see at the end of the function, if I didn’t, it will be dropped by the time it’s sent to the client.
In my replication model, the players are represented as data on the server, so having creating a model on the server causes issues since it’s automatically replicated to all players, so now each client would have two copies of each player’s avatar model.
this function doesn’t work on the client, apparently you can’t use :LoadAsset method of asset service on the client.
local function onGetPlayerModelRequest(_, playerId: number): Model
assert(typeof(playerId) == "number", "Player Id must be a number")
local avatarData = game.Players:GetHumanoidDescriptionFromUserId(playerId)
local character = DefaultRig:Clone()
character.Humanoid:ApplyDescription(avatarData)
character.Parent = workspace
return character
end
Data Preparation: Prepare and send only the necessary avatar data to the client.
local RepStorage = game:GetService("ReplicatedStorage")
local GetPlayerModelDataRequest = RepStorage.GetPlayerModelDataRequest
local function onGetPlayerModelDataRequest(_, playerId: number)
assert(typeof(playerId) == "number", "Player Id must be a number")
local avatarData = PlayersService:GetCharacterAppearanceInfoAsync(playerId)
return avatarData
end
GetPlayerModelDataRequest.OnServerInvoke = onGetPlayerModelDataRequest
Client-side
Model Construction: Construct the model locally on each client.
local RepStorage = game:GetService("ReplicatedStorage")
local DefaultRig = script.DefaultRig
local GetPlayerModelDataRequest = RepStorage.GetPlayerModelDataRequest
local function constructCharacterModel(playerId)
local avatarData = GetPlayerModelDataRequest:InvokeServer(playerId)
local character = DefaultRig:Clone()
-- Set character properties based on data
character.Name = PlayersService:GetNameFromUserIdAsync(playerId)
-- Apply Body Colors
local bodyColors = Instance.new("BodyColors")
bodyColors.HeadColor = BrickColor.new(avatarData.bodyColors.headColorId)
bodyColors.TorsoColor = BrickColor.new(avatarData.bodyColors.torsoColorId)
bodyColors.LeftArmColor = BrickColor.new(avatarData.bodyColors.leftArmColorId)
bodyColors.RightArmColor = BrickColor.new(avatarData.bodyColors.rightArmColorId)
bodyColors.LeftLegColor = BrickColor.new(avatarData.bodyColors.leftLegColorId)
bodyColors.RightLegColor = BrickColor.new(avatarData.bodyColors.rightLegColorId)
bodyColors.Parent = character
-- Load accessories, clothing, and body parts from assets
for _, asset in ipairs(avatarData.assets) do
local assetModel = InsertService:LoadAsset(asset.id) -- This will need a workaround as InsertService isn't available on the client
for _, loadedAsset in ipairs(assetModel:GetChildren()) do
for _, part: Part in loadedAsset:GetDescendants() do
if part:IsA("Part") then
part.CastShadow = false
part.CanCollide = false
part.CanQuery = false
part.CanTouch = false
end
end
loadedAsset.Parent = character
end
end
-- Parent to the client-specific location
character.Parent = workspace -- Adjust this to wherever you need the model on the client
end
local InsertService = game:GetService("InsertService")
local PlayersService = game:GetService("Players")
local RepStorage = game:GetService("ReplicatedStorage")
local GetPlayerModelRequest = RepStorage.GetPlayerModelRequest
local DefaultRig = script.DefaultRig
local function onGetPlayerModelRequest(_, playerId: number): Model
assert(typeof(playerId) == "number", "Player Id must be a number")
local avatarData = PlayersService:GetCharacterAppearanceInfoAsync(playerId)
local character = DefaultRig:Clone()
local scale = character:GetScale()
character:ScaleTo(1)
character.Parent = game.ReplicatedStorage
character.Name = PlayersService:GetNameFromUserIdAsync(playerId)
local humanoid = character.Humanoid or Instance.new("Humanoid")
local alreadyThere = character:FindFirstChild("Humanoid")
humanoid.Parent = character
local descr = game.Players:GetHumanoidDescriptionFromUserId(playerId)
humanoid:ApplyDescription(descr)
character:ScaleTo(scale)
if not alreadyThere then
humanoid:Destroy()
end
character.Parent = workspace
return character
end
GetPlayerModelRequest.OnServerInvoke = onGetPlayerModelRequest
hopefully it works for you too, this one parents the character to replicatedStorage for it to have a valid DataModel
This is a different way of doing the same thing, I tried it and it works but it doesn’t solve the problem.
I guess If there is no way around not creating the model on the server, I might aswell just allow the server to create it for me and clone it on the client.
You are still using default Roblox character models, right? Not your own build?
You’ll likely need to do this on the server for a neater implementation. Creating a rig from a player’s UserId can be done simply like this:
--do this on the server
local players = game:GetService("Players")
local function onPlayerAdded(player: Player)
local char = players:CreateHumanoidModelFromUserId(player.UserId)
(player.Character or player.CharacterAdded:Wait()):Destroy()
char.Parent = workspace
end
players.PlayerAdded:Connect(onPlayerAdded)
The client can then wait for it in the workspace and do what it will.
Or, if you literally just don’t want the player to have default Roblox control over the model, you can set the player.Character property to something else.
Players:GetHumanoidDescriptionFromUserId()
to get a humanoid description object, it has all the Id’s of all the assets, body colors, etc of the player’s avatar.
Then I can apply it use Humanoid:ApplyDescription()
the humanoid has to be created locally though, so an already exisiting humanoid when the game was loaded won’t allow this method to work on it.