Allow Changing Avatar Type (R15 <- -> R6) Without Shutting Down Servers

As a Roblox developer, it is currently too hard to change an experience’s avatar type without shutting down all servers.

Currently, my experience is strictly R15, but I want to broaden support for more avatar types by allowing R15 & R6. I have already implemented full support for this, but in order to enable it, I am prompted to shut down all servers.

This is incredibly unintuitive as with all of our game updates, we migrate rather than kicking players out of our experience. Sometimes we even go as far as pushing an entire update live without a migration. This is all to provide a better experience for end-users.

Due to the required shutdown to transition to R15 & R6, I have been put away from enabling this feature for months. In the event that an issue happens while enabling support, I am worried that I would have to do ANOTHER shutdown to revert R6 support.

I have looked into changing avatar types programmatically, but all the solutions I have found ended up being hacky or unstable, and is also an odd approach to something that should be trivial to setup.

If there was a better way to change avatar types, this would better my capabilities of providing a diverse atmosphere for players to express themselves in.

6 Likes

I’m not sure if there’s a way to check if the player’s avatar is R6 or R15 outside of the game, but, if you can, you can use a custom StarterCharacter that uses R6 and apply their HumanoidDescription to it, or otherwise change the player’s character to an R6 model (by changing the Character property of the player) and do the same. It seems to apply the description as accurately it can, based on whatever the avatar is.

I’ll keep looking and edit this reply if I find a solid solution.

2 Likes

That was the same consensus I came to, but I’m not a huge fan of it because it just seems a little unreliable, especially if there’s API outages. I’m just very scared of doing something like that because of the tree of potential issues that could arise from it, if that makes sense!

1 Like

I think API failure would be okay though, as you can wrap that in a pcall. If it fails, just don’t do anything to their avatar. Once the HumanoidDescription is successful, it’s just an object with data, so there shouldn’t be any risk after then. I can understand possibly having other risks if your game isn’t expecting avatar changes and your code really cares about that.

In any case, discovering their avatar type externally would be the key for this anyway. HumanoidDescription doesn’t seem to come with any direct information regarding whether the avatar type is R6 or R15 sadly. I’ll still be looking though.


I’m not sure if this would be affected by the game’s character type selection, but it appears that CreateHumanoidModelFromUserId can allow you to spawn their character in, which you can check its Humanoid for the rig type, or just switch their character to that. There’s also CreateHumanoidModelFromDescription, where you can force it into a specific rig type.

With this in mind, I’d probably have a system where CharacterAutoLoads is disabled and you have a script that sets things up before allowing them to spawn. Just know that anything in StarterGui will not be cloned until their character as spawned, but you can still insert Gui in via scripts.

2 Likes

Here’s what I’ve come up with so far:

v1 (Basic and incomplete)

(I’m currently editing it to improve the logic and also handle respawns, so don’t use this directly yet.)

local players = game:GetService('Players')
players.CharacterAutoLoads = false

function setSpawnLocation(character:Model)
	-- Modify for your spawning logic
	local spawnLocation = workspace:FindFirstChildOfClass('SpawnLocation')
	if spawnLocation then
		character:MoveTo(spawnLocation.Position)
	else
		character:MoveTo(Vector3.zero)
	end
end

players.PlayerAdded:Connect(function(player)
	local _,character = pcall(function()
		return players:CreateHumanoidModelFromUserId(player.UserId)
	end)
	if character then
		character.Name = player.Name
		player.Character = character
		character.Parent = workspace
		setSpawnLocation(character)
	else
		player:LoadCharacter()
	end
end)

v2:

(May still need some further testing, but it now supports: Teams, randomized spawns, spawn rotation, and respawning behavior.)

local players = game:GetService('Players')
players.CharacterAutoLoads = false

local spawnerList = {}
spawnerList.Neutral = {}
spawnerList.Teams = {}

local teams = game:GetService('Teams')
local function getTeamFromSpawner(spawner:SpawnLocation):Team?
	for _,team in teams:GetTeams()::{Team} do
		if team.TeamColor == spawner.TeamColor then
			return team
		end
	end
	return nil
end

local function addSpawner(spawner:SpawnLocation)
	if spawner:IsA('SpawnLocation') then
		if not spawner.Neutral then
			local team = getTeamFromSpawner(spawner)
			if team then
				local teamColor = team.TeamColor.Name
				if not spawnerList.Teams[teamColor] then
					spawnerList.Teams[teamColor] = {}
				end
				table.insert(spawnerList.Teams[teamColor],spawner)
				return
			end
		end
		table.insert(spawnerList.Neutral,spawner)
	end
end
workspace.DescendantAdded:Connect(function(spawner:SpawnLocation)
	addSpawner(spawner)
end)
for _,spawner:SpawnLocation in workspace:GetDescendants() do
	addSpawner(spawner)
end

workspace.DescendantRemoving:Connect(function(spawner:SpawnLocation)
	if spawner:IsA('SpawnLocation') then
		local team = getTeamFromSpawner(spawner)
		if team then
			local teamColor = team.TeamColor.Name
			local indexTeam = table.find(spawnerList.Teams[teamColor],spawner)
			if indexTeam then
				table.remove(spawnerList.Teams[teamColor],indexTeam)
			end
			if #spawnerList.Teams[teamColor] == 0 then
				spawnerList.Teams[teamColor] = nil
			end
		else
			local indexNeutral = table.find(spawnerList.Neutral,spawner)
			if indexNeutral then
				table.remove(spawnerList.Neutral,indexNeutral)
			end
		end
	end
end)

local function setSpawnLocation(player:Player,character:Model)
	-- Modify for your spawning logic
	local function moveToSpawner(spawner:SpawnLocation)
		character:MoveTo(spawner.Position)
		local humanoidRootPart = character:WaitForChild('HumanoidRootPart')::Part
		task.wait()
		humanoidRootPart.CFrame = CFrame.new(humanoidRootPart.Position)*CFrame.Angles(0,math.rad(spawner.Orientation.Y),0)
	end
	
	local playerTeam = player.Team
	if playerTeam then
		local teamTable = spawnerList.Teams[playerTeam.TeamColor.Name]
		if teamTable then
			local teamTableCount = #teamTable
			if teamTableCount > 0 then
				local randomizedSpawner = teamTable[math.random(1,teamTableCount)]
				moveToSpawner(randomizedSpawner)
				return
			end
		end
	end
	
	local neutralCount = #spawnerList.Neutral
	if neutralCount > 0 then
		local randomizedSpawner = spawnerList.Neutral[math.random(1,neutralCount)]
		moveToSpawner(randomizedSpawner)
		return
	end
	
	character:MoveTo(Vector3.zero)
end

local function forceRespawn(player:Player)
	local _,character = pcall(function()
		return players:CreateHumanoidModelFromUserId(player.UserId)
	end)
	if character then
		local humanoid = character:WaitForChild('Humanoid')::Humanoid
		local diedEvent,removingEvent
		local function eventFunction()
			diedEvent:Disconnect()
			removingEvent:Disconnect()
			task.wait(players.RespawnTime)
			forceRespawn(player)
		end
		diedEvent = humanoid.Died:Connect(eventFunction)
		removingEvent = character:GetPropertyChangedSignal('Parent'):Connect(function()
			if not character.Parent then
				eventFunction()
			end
		end)
		character.Name = player.Name
		player.Character = character
		character.Parent = workspace
		setSpawnLocation(player,character)
	else
		player:LoadCharacter()
	end
end

players.PlayerAdded:Connect(function(player)
	forceRespawn(player)
end)

I don’t like that I have to write my own custom spawning logic, but perhaps there’s a better method. This may also fail to trigger certain events, though CharacterAdded still seems to work.


Definitely test before using in a live server, obviously. I’ve tested this exclusively in Studio. However, this was not tested in an environment where R15 is set as the only rig type allowed, so I’m not sure if that will alter this or not.

I’ve now tested in a live server with the rig type exclusive to one or the other. This script appears to work, allowing me to join with R6 or R15 and having either one appear, regardless of the game settings.

5 Likes