Coding a Murder Mystery Game - In-depth Coding Tutorial

Introduction

Hello everyone! In this topic, you’ll be learning to create a murder mystery game! This took me about 6-7 hours in total (writing and coding of this tutorial) so I’ve put a lot of work and effort into this! This code is considered a beginner/intermediate level. Anywho, if there are any questions and/or concerns, please let me know! Onto coding!

Part 0 - Getting started

In order to actually start coding, you’ll need the assets to work with. Go here and edit it: Murder Mystery Template It’s uncopylocked too! Open it and move onto the actual coding!

Part 1 - Coding the backend

As you can see, in ServerStorage, you can see the map. You can also see in ReplicatedStorage, an event, items, and status value in the Value folder. We will need to go to master in ServerScriptService to code access to the status stringvalue and require the module that is parented to it. To do so, we will need to code this:

local Status = game.ReplicatedStorage.Values:WaitForChild("status")
local round = require(script.Round)

Now, since this is a round-based game, we will need this game to run forever. We will need to add in a while loop below the variables like so:

local Status = game.ReplicatedStorage.Values:WaitForChild("status")
local round = require(script.Round)

while wait() do

end

Next, in all most games, in order to begin it, you’ll need to detect if there are 2 or more players in the game. in order to do so, we will need to put this into repeat-until loop. This way, it’ll wait until there are two players. Inside of this loop, we will need a table and add the players into that table. This way, we can detect how many players are actually ready. We will need to also add in a for loop through all of the players and add them into the table. Your code should look like this:

local Status = game.ReplicatedStorage.Values:WaitForChild("status")
local round = require(script.Round)

while wait() do
	repeat
		local availablePlayers = {}

		for i, plr in pairs(game.Players:GetPlayers()) do
			table.insert(availablePlayers, plr)
		end
		Status.Value = "In order for the round to begin, there must be at least 2 players in the server."
		wait(2)
	until #availablePlayers >= 2

end

Now that we have that, we need to move to the Round module. Modules allow you to create functions to which you can call those functions. In our case, we will need to create a function that will run intermission. We want to add a parameter so taht we can set any value for the countdown. We will need to also add in a for loop to count it down. We will also need to set the status equal to the text like this (MAKE SURE YOU CALL FOR THE STATUS STRING VALUE!!): Next round begins in… . To code this, do the following:

local status = game.ReplicatedStorage.Values:WaitForChild("status")
function module.Intermission(length)
	for i = length, 0, -1 do
		status.Value = "Next round in "..i.." seconds"
		wait(1)
	end
end

Nice! Now we just need to call this function in our main script so that it runs intermission. Since length is our parameter, set it to a positive integer. For me, I’ll set it to 5. In Master, type the following:

local Status = game.ReplicatedStorage.Values:WaitForChild("status")
local round = require(script.Round)

while wait() do
	repeat
		local availablePlayers = {}

		for i, plr in pairs(game.Players:GetPlayers()) do
			table.insert(availablePlayers, plr)
		end
		Status.Value = "In order for the round to begin, there must be at least 2 players in the server."
		wait(2)
	until #availablePlayers >= 2
	
	round.Intermission(5)

end

Now, we will need to code a function for choosing a random map. If you look in ReplicatedStorage, you can see the map. It may look very similar to you. It’s because it was created by @Stickmasterluke back when he created the Mad Bloxxer tutorial series! Now, we will need to create the function in the module. It needs to choose a random map from that folder. Do the following in that script:

function module.SelectMap()
	local random = Random.new()
	local maps = game.ReplicatedStorage.Maps:GetChildren() -- Table of all of the maps
	local chosenMap = maps[random:NextInteger(1,#maps)]

	return chosenMap
end

It’s going to return a special value to us, the map’s name! Now, we need to get that in the Master code, clone it, and name it Map (THIS IS VERY IMPORTANT FOR TELEPORTING PLAYERS!!!). Do the following code:

local Status = game.ReplicatedStorage.Values:WaitForChild("status")
local round = require(script.Round)

while wait() do
	repeat
		local availablePlayers = {}

		for i, plr in pairs(game.Players:GetPlayers()) do
			table.insert(availablePlayers, plr)
		end
		Status.Value = "In order for the round to begin, there must be at least 2 players in the server."
		wait(2)
	until #availablePlayers >= 2
	
	round.Intermission(5)

	local chosenMap = round.SelectMap()

	local clonedMap = chosenMap:Clone()
	clonedMap.Name = "Map"
	clonedMap.Parent = game.Workspace

end

Now that we have that, we will need to identify both the sheriff and murderer. In round module, we will need to decide who it will be using the random.new() function. In the code:

function module.ChooseMurderer(players)
	local RandomObj = Random.new()

	local chosenMurderer = players[RandomObj:NextInteger(1, #players)]

	return chosenMurderer
end
function module.ChooseSheriff(players)
	local RandomObj = Random.new()

	local chosenSheriff = players[RandomObj:NextInteger(1, #players)]

	return chosenSheriff
end

We will need the players function for a very important reason! We need to identify all of the players and put them into a table. This way, one person can be a murderer and another becomes a sheriff. Please, do the following in master:

local Status = game.ReplicatedStorage.Values:WaitForChild("status")
local round = require(script.Round)

while wait() do
	repeat
		local availablePlayers = {}

		for i, plr in pairs(game.Players:GetPlayers()) do
			table.insert(availablePlayers, plr)
		end
		Status.Value = "In order for the round to begin, there must be at least 2 players in the server."
		wait(2)
	until #availablePlayers >= 2
	
	round.Intermission(5)

	local chosenMap = round.SelectMap()

	local clonedMap = chosenMap:Clone()
	clonedMap.Name = "Map"
	clonedMap.Parent = game.Workspace
	
	local innocentPlayers = {}
	local murderer = {}
	local sheriff = {}

	for i, v in pairs(game.Players:GetPlayers()) do
		table.insert(innocentPlayers, v)
	end
	
	local chosenMurdererPlayer = round.ChooseMurderer(innocentPlayers)
	
	for i, v in pairs(innocentPlayers) do
		if v == chosenMurdererPlayer then
			table.remove(innocentPlayers,i)
			table.insert(murderer, i)
		end
	end

	
	local chosenSheriffPlayer = round.ChooseSheriff(innocentPlayers)
	
	for i, v in pairs(innocentPlayers) do
		if v == chosenSheriffPlayer then
			table.remove(innocentPlayers,i)
			table.insert(sheriff, i)
		end
	end
	wait(2)

We identified the innocent players, murderer, and sheriff by removing both the murderer and sheriff out of the innocent players table. Next we will need to havea string value attached to each player identifying who they are so that our gun can identify who is who when shooting. Create the function in round:

function module.InsertTag(players, tagName)
	for i, player in pairs(players) do
		local Tag = Instance.new("StringValue")
		Tag.Name = tagName
		Tag.Parent = player
	end
end

Then, add this into master by giving the murderer, sheriff, and innocent users their respective tags like so:

    round.InsertTag(innocentPlayers, "Innocent")
	round.InsertTag({chosenSheriffPlayer}, "Sheriff")
	round.InsertTag({chosenMurdererPlayer}, "Murderer")

Now, we need to teleport everyone as well as their items! The murderer will have a cool sword while the sheriff gets the gun. Do the following in round module:

function module.TeleportMurderer(player, mapSpawns)
	if player.Character then
		local knife = game.ServerStorage.Items.ClassicSword:Clone()
		knife.Parent = player.Backpack

		if player.Character:FindFirstChild("HumanoidRootPart") then
			local rand = Random.new()
			player.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1, #mapSpawns)].CFrame + Vector3.new(0,10,0)
		end

	end
end
function module.TeleportSheriff(player, mapSpawns)
	if player.Character then
		local gun = game.ServerStorage.Items.Revolver:Clone()
		gun.Parent = player.Backpack

		if player.Character:FindFirstChild("HumanoidRootPart") then
			local rand = Random.new()
			player.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1, #mapSpawns)].CFrame + Vector3.new(0,10,0)
		end

	end
end

We will also need to teleport the innocent users. Because there is more than one of them, it’ll be a little different:

function module.TeleportPlayers(player, mapSpawns)
	for i, player in pairs(player) do
		if player.Character then
			local character = player.Character

			if character:FindFirstChild("HumanoidRootPart") then
				local rand = Random.new()
				player.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1, #mapSpawns)].CFrame + Vector3.new(0,10,0)
			end
		end
	end
end

local function toMS(s)
	return ("%02i:%02i"):format(s/60%60, s%60)
end

Now, we will need to call this in master! Do the following:


	if clonedMap:FindFirstChild("PlayerSpawns") then
		round.TeleportPlayers(innocentPlayers, clonedMap.PlayerSpawns:GetChildren())
		round.TeleportSheriff(chosenSheriffPlayer, clonedMap.PlayerSpawns:GetChildren())
		round.TeleportMurderer(chosenMurdererPlayer, clonedMap.PlayerSpawns:GetChildren())
	else
		warn("Fatal Error: no PlayerSpawns Folder")
	end

Now that everyone is teleported, we need to start the round! We will need a parameter for identifying the time, murderer, and map (just in case they weren’t teleported). We will need to code the outcomes too such as who won and time being possible up. Earlier, you may have seen this function:

local function toMS(s)
	return ("%02i:%02i"):format(s/60%60, s%60)
end

This function coverts seconds to minutes. It’s very important for starting the round in the function in round module:

function module.StartRound(length, murderer, map) -- in seconds
	local outcome
	
	game.ReplicatedStorage.Events.IdentifyPlayer:FireAllClients()
	
	for i = length, 0, -1 do
		local innocentPlayers = {}
		local murderer = {}
		local murdererHere = false
		
		module.TeleportPlayers({murderer}, map.PlayerSpawns:GetChildren())

		for i, player in pairs(game.Players:GetPlayers()) do
			if player:FindFirstChild("Innocent") then
				table.insert(innocentPlayers, player)
			elseif player:FindFirstChild("Sheriff") then
				table.insert(innocentPlayers, player)
			elseif player:FindFirstChild("Murderer") then
				wait(.3)
				murdererHere = true
				table.insert(murderer, player)
			end
		end

		status.Value = toMS(i)

		if not murdererHere then
			outcome = "murderer-left"
			break
		end
		
		if #murderer == 0 then
			outcome = "murderer-died"
			break
		end

		if #innocentPlayers == 0 then
			outcome = "murderer-wins"
			break
		end

		if i == 0 then
			outcome = "time-up"
			break
		end

		wait(1)
	end


	if outcome == "murderer-wins" then
		status.Value = "Murderer wins!"
	elseif outcome == "time-up" then
		status.Value = "Times up!"
	elseif outcome == "murderer-left" then
		status.Value = "Murderer lost! Innocents win!"
	elseif outcome == "murderer-died" then
		status.Value = "Murderer has died! Innocents win!"
	end

	wait(5)

end

Now, we just need to call it in master! Like so:

	round.StartRound(600, chosenMurdererPlayer, clonedMap)

Now! Since the outcomes will occur, we will need to end the game at some point. We will need to remove all of the items and return everyone back to the lobby. Do the following:

function module.RemoveTags()
	for i, v in pairs(game.Players:GetPlayers()) do
		if v:FindFirstChild("Murderer") then
			v.Murderer:Destroy()
			if v.Backpack:FindFirstChild("ClassicSword") then v.Backpack.ClassicSword:Destroy() end
			if v.Character:FindFirstChild("ClassicSword") then v.Character.ClassicSword:Destroy() end
			v:LoadCharacter()

		end

		if v:FindFirstChild("Innocent") then
			v.Innocent:Destroy()

			for _, p in pairs(v.Backpack:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end

			for _, p in pairs(v.Character:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end
			v:LoadCharacter()

		end
		if v:FindFirstChild("Sheriff") then
			v.Sheriff:Destroy()
		v:LoadCharacter()

			for _, p in pairs(v.Backpack:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end

			for _, p in pairs(v.Character:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end
			v:LoadCharacter()

		end
		
		v:LoadCharacter()
	end

end

Now, we just need to call this function as well as destroy the map like so:

clonedMap:Destroy()
	
	round.RemoveTags()
	
	wait(2)

Finally, if the sheriff dies, we will need to make sure that the gun can still be accessed by innocent users. In EventHandling, type the following:

game.Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		character:WaitForChild("Humanoid").Died:Connect(function()
			if player:FindFirstChild("Murderer") ~= nil then
				player:FindFirstChild("Murderer"):Destroy()
			elseif player:FindFirstChild("Sheriff") ~= nil then
				player:FindFirstChild("Sheriff"):Destroy()
				for _, p in pairs(player.Backpack:GetChildren()) do
					if p:IsA("Tool") then
						local decoyGun = game.ServerStorage.Items.DecoyGun:Clone()
						decoyGun.Parent = game.Workspace:FindFirstChild("Map")
						decoyGun.CFrame = character.HumanoidRootPart.CFrame + Vector3.new(0,2,0)
						p:Destroy()
					end
				end
			elseif 	player:FindFirstChild("Innocent") ~= nil then
				player:FindFirstChild("Innocent"):Destroy()
			end
		end)
	end)
end)

Congratulations! You have the backend stuff coded. if you had troubles or just want to make sure your code is correct, take a look at the key below

MASTER:

local Status = game.ReplicatedStorage.Values:WaitForChild("status")
local round = require(script.Round)

while wait() do
	repeat
		local availablePlayers = {}

		for i, plr in pairs(game.Players:GetPlayers()) do
			table.insert(availablePlayers, plr)
		end
		Status.Value = "In order for the round to begin, there must be at least 2 players in the server."
		wait(2)
	until #availablePlayers >= 2
	
	round.Intermission(5)

	local chosenMap = round.SelectMap()

	local clonedMap = chosenMap:Clone()
	clonedMap.Name = "Map"
	clonedMap.Parent = game.Workspace
	
	local innocentPlayers = {}
	local murderer = {}
	local sheriff = {}

	for i, v in pairs(game.Players:GetPlayers()) do
		table.insert(innocentPlayers, v)
	end
	
	local chosenMurdererPlayer = round.ChooseMurderer(innocentPlayers)
	
	for i, v in pairs(innocentPlayers) do
		if v == chosenMurdererPlayer then
			table.remove(innocentPlayers,i)
			table.insert(murderer, i)
		end
	end

	
	local chosenSheriffPlayer = round.ChooseSheriff(innocentPlayers)
	
	for i, v in pairs(innocentPlayers) do
		if v == chosenSheriffPlayer then
			table.remove(innocentPlayers,i)
			table.insert(sheriff, i)
		end
	end
	wait(2)
	
	round.InsertTag(innocentPlayers, "Innocent")
	round.InsertTag({chosenSheriffPlayer}, "Sheriff")
	round.InsertTag({chosenMurdererPlayer}, "Murderer")
	
	if clonedMap:FindFirstChild("PlayerSpawns") then
		round.TeleportPlayers(innocentPlayers, clonedMap.PlayerSpawns:GetChildren())
		round.TeleportSheriff(chosenSheriffPlayer, clonedMap.PlayerSpawns:GetChildren())
		round.TeleportMurderer(chosenMurdererPlayer, clonedMap.PlayerSpawns:GetChildren())
	else
		warn("Fatal Error: no PlayerSpawns Folder")
	end
	
	round.StartRound(600, chosenMurdererPlayer, clonedMap)
	
	clonedMap:Destroy()
	
	round.RemoveTags()
	
	wait(2)
end

ROUND MODULE (PARENTED TO MASTER):

local module = {}
local status = game.ReplicatedStorage.Values:WaitForChild("status")
function module.Intermission(length)
	for i = length, 0, -1 do
		status.Value = "Next round in "..i.." seconds"
		wait(1)
	end
end
function module.SelectMap()
	local random = Random.new()
	local maps = game.ReplicatedStorage.Maps:GetChildren() -- Table of all of the maps
	local chosenMap = maps[random:NextInteger(1,#maps)]

	return chosenMap
end
function module.ChooseMurderer(players)
	local RandomObj = Random.new()

	local chosenMurderer = players[RandomObj:NextInteger(1, #players)]

	return chosenMurderer
end
function module.ChooseSheriff(players)
	local RandomObj = Random.new()

	local chosenSheriff = players[RandomObj:NextInteger(1, #players)]

	return chosenSheriff
end
function module.InsertTag(players, tagName)
	for i, player in pairs(players) do
		local Tag = Instance.new("StringValue")
		Tag.Name = tagName
		Tag.Parent = player
	end
end
function module.TeleportMurderer(player, mapSpawns)
	if player.Character then
		local knife = game.ServerStorage.Items.ClassicSword:Clone()
		knife.Parent = player.Backpack

		if player.Character:FindFirstChild("HumanoidRootPart") then
			local rand = Random.new()
			player.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1, #mapSpawns)].CFrame + Vector3.new(0,10,0)
		end

	end
end
function module.TeleportSheriff(player, mapSpawns)
	if player.Character then
		local gun = game.ServerStorage.Items.Revolver:Clone()
		gun.Parent = player.Backpack

		if player.Character:FindFirstChild("HumanoidRootPart") then
			local rand = Random.new()
			player.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1, #mapSpawns)].CFrame + Vector3.new(0,10,0)
		end

	end
end
function module.TeleportPlayers(player, mapSpawns)
	for i, player in pairs(player) do
		if player.Character then
			local character = player.Character

			if character:FindFirstChild("HumanoidRootPart") then
				local rand = Random.new()
				player.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1, #mapSpawns)].CFrame + Vector3.new(0,10,0)
			end
		end
	end
end

local function toMS(s)
	return ("%02i:%02i"):format(s/60%60, s%60)
end

function module.StartRound(length, murderer, map) -- in seconds
	local outcome
	
	game.ReplicatedStorage.Events.IdentifyPlayer:FireAllClients()
	
	for i = length, 0, -1 do
		local innocentPlayers = {}
		local murderer = {}
		local murdererHere = false
		
		module.TeleportPlayers({murderer}, map.PlayerSpawns:GetChildren())

		for i, player in pairs(game.Players:GetPlayers()) do
			if player:FindFirstChild("Innocent") then
				table.insert(innocentPlayers, player)
			elseif player:FindFirstChild("Sheriff") then
				table.insert(innocentPlayers, player)
			elseif player:FindFirstChild("Murderer") then
				wait(.3)
				murdererHere = true
				table.insert(murderer, player)
			end
		end
		print(innocentPlayers[1])

		status.Value = toMS(i)

		if not murdererHere then
			outcome = "murderer-left"
			break
		end
		
		if #murderer == 0 then
			outcome = "murderer-died"
			break
		end

		if #innocentPlayers == 0 then
			outcome = "murderer-wins"
			break
		end

		if i == 0 then
			outcome = "time-up"
			break
		end

		wait(1)
	end


	if outcome == "murderer-wins" then
		status.Value = "Murderer wins!"
	elseif outcome == "time-up" then
		status.Value = "Times up!"
	elseif outcome == "murderer-left" then
		status.Value = "Murderer lost! Innocents win!"
	elseif outcome == "murderer-died" then
		status.Value = "Murderer has died! Innocents win!"
	end

	wait(5)

end 
function module.RemoveTags()
	for i, v in pairs(game.Players:GetPlayers()) do
		if v:FindFirstChild("Murderer") then
			v.Murderer:Destroy()
			if v.Backpack:FindFirstChild("ClassicSword") then v.Backpack.ClassicSword:Destroy() end
			if v.Character:FindFirstChild("ClassicSword") then v.Character.ClassicSword:Destroy() end
			v:LoadCharacter()

		end

		if v:FindFirstChild("Innocent") then
			v.Innocent:Destroy()

			for _, p in pairs(v.Backpack:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end

			for _, p in pairs(v.Character:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end
			v:LoadCharacter()

		end
		if v:FindFirstChild("Sheriff") then
			v.Sheriff:Destroy()
		v:LoadCharacter()

			for _, p in pairs(v.Backpack:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end

			for _, p in pairs(v.Character:GetChildren()) do
				if p:IsA("Tool") then
					p:Destroy()
				end
			end
			v:LoadCharacter()

		end
		
		v:LoadCharacter()
	end

end
return module

EVENTHANDLING:

game.Players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		character:WaitForChild("Humanoid").Died:Connect(function()
			if player:FindFirstChild("Murderer") ~= nil then
				player:FindFirstChild("Murderer"):Destroy()
			elseif player:FindFirstChild("Sheriff") ~= nil then
				player:FindFirstChild("Sheriff"):Destroy()
				for _, p in pairs(player.Backpack:GetChildren()) do
					if p:IsA("Tool") then
						local decoyGun = game.ServerStorage.Items.DecoyGun:Clone()
						decoyGun.Parent = game.Workspace:FindFirstChild("Map")
						decoyGun.CFrame = character.HumanoidRootPart.CFrame * CFrame.new(0,2,0)
						p:Destroy()
					end
				end
				for _, p in pairs(player.Character:GetChildren()) do
					if p:IsA("Tool") then
						local decoyGun = game.ServerStorage.Items.DecoyGun:Clone()
						decoyGun.Parent = game.Workspace:FindFirstChild("Map")
						decoyGun.CFrame = character.HumanoidRootPart.CFrame * CFrame.new(0,2,0)
						p:Destroy()
					end
				end
				end
			elseif 	player:FindFirstChild("Innocent") ~= nil then
				player:FindFirstChild("Innocent"):Destroy()
			end
		end)
	end)
end)

Part 2 - UI

Now that we have the actual game done, we need to code the UI related things such as the status bar and identifying the player. You may have seen the remote event called earlier, it’s for the identifying he player box. Code the following in the local script in StarterGUI:

local replicatedStorage = game:GetService("ReplicatedStorage")
local status = replicatedStorage.Values:WaitForChild("status")

local topStatus = script.Parent:WaitForChild("Status")
topStatus.Text = status.Value
topStatus.Visible = true

local IdentificationBox = script.Parent.PlayerIdentification

status:GetPropertyChangedSignal("Value"):Connect(function()
	topStatus.Text = status.Value
end)

game.ReplicatedStorage.Events.IdentifyPlayer.OnClientEvent:Connect(function()
	local player = game.Players.LocalPlayer
	IdentificationBox.Visible = true
	wait(2)
	if player:FindFirstChild("Murderer") then
		IdentificationBox.Murderer.Visible = true
	elseif player:FindFirstChild("Sheriff") then
		IdentificationBox.Sheriff.Visible = true
	elseif player:FindFirstChild("Innocent") then
		IdentificationBox.Innocent.Visible = true
	end
	wait(2)
	IdentificationBox.Visible = false
	IdentificationBox.Murderer.Visible = false
	IdentificationBox.Sheriff.Visible = false
	IdentificationBox.Innocent.Visible = false
end)

Extra Features

These are extra things that you can add to the game to make it a lot more enjoyable! For starters, please get this model: Murder Mystery Tuturial - Addons - Roblox

Please put the available characters folder into ReplicatedStorage and the MurderChance into StarterGui > ScreenGui. You can delete that model that was a part of the model you got as it’s no longer needed.

Characters
To start, go into the EventHandling. We will need to have a string value for the character that’s chosen so it’s easier to dress up the player. In the PlayerAdded function, add the following:

	local Character = Instance.new("StringValue")
	Character.Name = "Character"
	Character.Value = "None"
	Character.Parent = player

Now, in the round module, we will need to decide what character each player gets. We don’t want duplicates either, so we will need to create a table like so:

function module.ChooseCharacter()
	local AvailableCharacters = {}
end

Now we will need to loop through each player and choose a character for the player to play as, do the following:

function module.ChooseCharacter()
	local AvailableCharacters = {}
	for i, v in pairs(game.ReplicatedStorage.AvailableCharacters:GetChildren()) do
		table.insert(AvailableCharacters, v.Name)
	end
	for i, v in pairs(game.Players:GetPlayers()) do
		local randomCharacter = math.random(1, #AvailableCharacters)
		
		local ChosenCharacter = AvailableCharacters[randomCharacter]
	end
end

Finally, we will need to set that random character to that character stringvalue value and remove that character from the table so no duplicates occur. Do the following:

function module.ChooseCharacter()
	local AvailableCharacters = {}
	for i, v in pairs(game.ReplicatedStorage.AvailableCharacters:GetChildren()) do
		table.insert(AvailableCharacters, v.Name)
	end
	for i, v in pairs(game.Players:GetPlayers()) do
		local randomCharacter = math.random(1, #AvailableCharacters)
		
		local ChosenCharacter = AvailableCharacters[randomCharacter]
		
		v:FindFirstChild("Character").Value = ChosenCharacter
		
		table.remove(AvailableCharacters, randomCharacter)
	end
end

Now that the player has a character chosen, we will need to dress that player. In the round module in a new function, do the following:

function module.DressPlayers()
	for i, player in pairs(game.Players:GetPlayers()) do
		local character

		if player:FindFirstChild("Character").Value ~= "" then
			character = game.ReplicatedStorage.AvailableCharacters[player:FindFirstChild("Character").Value]:Clone()
		end
		character.Name = player.Name
		player.Character = character
		character.Parent = workspace
	end
end

What this will do is check to see if it exists. I’d that character exists, set the player as the character.

Add these two functions after choosing the sheriff in master like here:

local chosenSheriffPlayer = round.ChooseSheriff(innocentPlayers)
	
	for i, v in pairs(innocentPlayers) do
		if v == chosenSheriffPlayer then
			table.remove(innocentPlayers,i)
			table.insert(sheriff, i)
		end
	end
	wait(.01)
	
	round.ChooseCharacter()
	round.DressPlayers()
	
	wait(.01)
	
	round.InsertTag(innocentPlayers, "Innocent")
	round.InsertTag({chosenSheriffPlayer}, "Sheriff")
	round.InsertTag({chosenMurdererPlayer}, "Murderer")

Murder Chance
Now, with that MurderChance textlabel, we will need to update the choosing the murderer function. We want it create a table and insert the number of times the player has for murder chance into that table. In the PlayerAdded function in EventHandling, create the following intvalue:

local MurderChance = Instance.new("IntValue")
	MurderChance.Name = "MurderChance"
	MurderChance.Value = 1
	MurderChance.Parent = player

Now in the Choose Murder function, do the following:

function module.ChooseMurderer(players)
	local RandomObj = Random.new()
	
	local PossibleMurderers = {}
	
	for _, v in pairs(players) do
		local MurderChance = v.MurderChance.Value
		for i = 1, MurderChance do
			table.insert(PossibleMurderers, v.UserId)
		end
	end

	local chosenMurderer = PossibleMurderers[RandomObj:NextInteger(1, #PossibleMurderers)]
	
	local ChosenMurdererPlayer = game.Players:GetPlayerByUserId(chosenMurderer)
	
	for _, v in pairs(players) do
		if v.UserId ~= ChosenMurdererPlayer then
			v.MurderChance.Value += 1
		end
	end
	
	ChosenMurdererPlayer.MurderChance.Value = 1

	return ChosenMurdererPlayer
end

What this will do is based on the number for the MurderChance, it will add the number of the playerids to the table. Then choose the id from the table. If the others weren’t chosen, the chance will go up by one and the murderer will be set back to 1. In the local script in StarterGui > ScreenGui, we want to show this value to the player and only show an updated number if the value property changes. Do the following in that script:

player:WaitForChild("MurderChance"):GetPropertyChangedSignal("Value"):Connect(function()
	script.Parent.MurderChance.Text = "Murder Chance: ".. player.MurderChance.Value .. "%"
end)

Nice work! You just added in character selction and murder chances to the game for more enjoyment!

Conclusion

Thank you so much for reading! This took me a while to finish but I am glad to have finished it! You may have noticed that this code is similar to @Alvin_Blox’s, however, it was used for the murder mystery game. All round-based games will have something similar to this, though it may be different based on developer’s preference. Anyways, thank you so much and if you wanted to try the real game, here is the link: Murder Mystery - Tutorial - Roblox

Thank you and happy developing!

~ WooleyWool

129 Likes

You are so kind, Thanks for sharing something that some people can’t do, I needed some of those codes to finish my games :slight_smile:

6 Likes

Thank you! It took a bit to do and I was super motivated in doing this. I did the best i could in explaining.

PS My hands are cramping so I hope the pain was well worth it!

6 Likes

Very well written; I’m sure you spent a lot of time writing this and making this. Great work! :sweat_smile:

6 Likes

Thank you! I tried to make sure it made sense! I hope you’ll use this!

5 Likes

Oh yes! A few things I’d like to suggest! I may possibly expand on this and add in playable characters, spectate, shop, etc. if I feel so. It’ll be a lot of fun adding those elements too. Feel free to suggest anything!

8 Likes

Looks like a decent system, but you should add a % system for the Murderer instead of picking random.

4 Likes

Could look into that, though this does get the job done.

4 Likes

MAJOR BUG FIX

If the sheriff were to be killed, originally, the script would create a gun above the player for all the innocents to get. But, if the sheriff was holding it, the clone wouldn’t be created. This would give an unfair advantage for the innocent. To fix this, add the following in EventHandling:

elseif player:FindFirstChild("Sheriff") ~= nil then
				player:FindFirstChild("Sheriff"):Destroy()
				for _, p in pairs(player.Backpack:GetChildren()) do
					if p:IsA("Tool") then
						local decoyGun = game.ServerStorage.Items.DecoyGun:Clone()
						decoyGun.Parent = game.Workspace:FindFirstChild("Map")
						decoyGun.CFrame = character.HumanoidRootPart.CFrame * CFrame.new(0,2,0)
						p:Destroy()
					end
				end
				for _, p in pairs(player.Character:GetChildren()) do
					if p:IsA("Tool") then
						local decoyGun = game.ServerStorage.Items.DecoyGun:Clone()
						decoyGun.Parent = game.Workspace:FindFirstChild("Map")
						decoyGun.CFrame = character.HumanoidRootPart.CFrame * CFrame.new(0,2,0)
						p:Destroy()
					end
				end
8 Likes

Wow very good! Thanks for adding an uncopylocked version so I can examine the code and review through it to understand more coding!

3 Likes

It’s kinda uncopylocked, the assets are there but the code is in the post. Have fun!

2 Likes

I was really waiting for this! Thanks!

2 Likes

New Update!!!

In the Extra Features section, you can now add in characters and murder chance to change up the game. A model is attached for you to get so that you can work off of those!

Let me know if there’s any problems or any concerns! Thanks!

3 Likes

Just reviewing, this is actually extremely inefficient.
First you could just use

repeat
    Status.Value = "In order for the round to begin, there must be at least 2 players in the server."
    wait(2)
until #game.Players:GetPlayers() > 2

(That removes the need for putting all the players into a new table)

And even then I would recommend not utilizing loops, and using events instead

game.Players.PlayerAdded:Connect(function(player)
   if #game.Players:GetPlayers() > 2 then
      startRounds()
   end
end

Just some feedback :stuck_out_tongue:

5 Likes

The problem with this code is it will only prompt if someone new joins, and if there’s a current game going on, it will just stop that game.

I appreciate the feedback nonetheless!!

4 Likes

getting this error for some reason ServerScriptService.Master.Round:50: attempt to index nil with 'Character' - Server - Round:50

Did you create the character string value in playeradded?

1 Like

What’s the error? Where are you experiencing it?

Please show me that line area for Round

1 Like

oh ive been getting it all wrong : / welp here is the actual error:

function module.TeleportSheriff(player, mapSpawns)
	if player.Character then --error is this (line 51)
		local gun = game.ServerStorage.Items.Revolver:Clone()
		gun.Parent = player.Backpack

		if player.Character:FindFirstChild("HumanoidRootPart") then
			local rand = Random.new()
			player.Character.HumanoidRootPart.CFrame = mapSpawns[rand:NextInteger(1, #mapSpawns)].CFrame + Vector3.new(0,10,0)
		end

	end
end