How to make a character selection system?

Hello, I’m working on a fighting game and I’m trying to make a character selection system because for testing purposes I had hard-coded a lot of things, but I think I’m at a point where I’m happy with the code and wanna start moving things into stuff I can retrieve, but I’m honestly a bit lost on what to do.

This is my current idea, but it honestly feels pretty sloppy and clunky to work with especially for what I wanna do which is to be able to have the client and server both be able to access it for information.

-- SERVER --
-- When player interacts with this NPC add the TestCharacter module to their player so that they can require it
if ancestorModel.Name == "Character Setter" then
	local playerObj = Server:GetPlayerByInstance(player)
	local module = ServerStorage.Characters.TestCharacter:Clone()

	module.Parent = playerObj.player

	local event = {}
	event.t = eventType.characterSelected
	event.module = module

	playerObj:sendEventToClient(event)	
end
-- CLIENT --
-- When we receive the module we can set a player class member to it
eventHandler[eventType.characterSelected] = function(event)
	player:setClass(event.module.new())
end
2 Likes

Not entirely sure what you’re asking for, but if you’re just asking for feedback on this set up in particular:

  • Not sure why you need to copy paste a module. Clients can see modules parented to other players just as well as they can see their own, so no security benefit either.

  • I feel like the way you set up your events are as a result of a framework/library you’re working with. As far as I’ve been able to tell from working with other projects with a similar approach, the only real purpose to set up events this way is for both game analytics (which can be done in other ways than this) and for some performance gains (when you’re using a single remote for everything, using fake enums for event types lead to less data being transfered for fast-firing remotes).
    This doesn’t seem like something that’d need this kind of set up, so if your library allows you to use a more regular approach it’d almost certainly be better.

  • I’m completely guessing here but it almost seems like the way your server-sided code is set up is intended to be copied across every other “character setter” NPCs, and simply filter the behavior out by checking the NPC’s name. This is a very yandere-dev way of setting things up and I’d heavily recommend you instead use a different approach instead. One way I’m fond of is to do something like this:

local NPCs = {-- If I want to add another NPC I just add it to this table
	TestCharacter = serverStorage.Characters.TestCharacter,
	--imagine more characters here
}

local function spokeToCharacterNPC(ancestorModel)-- Should ideally never have to touch this function to add new characters in
	local chosenNPC = NPCs[ancestorModel.Name]-- Should return our testcharacter module
	if not chosenNPC then-- Did not find a valid NPC
		return
	end
	-- do your regular stuff here

	local playerObj = Server:GetPlayerByInstance(player)
	local module = chosenNPC:Clone()

	module.Parent = playerObj.player

	local event = {}
	event.t = eventType.characterSelected
	event.module = module

	playerObj:sendEventToClient(event)	
end
1 Like

Instead of just having 20 scripts on the client I just thought it’d be better to keep them on the server and give them to the clients when they’re actually needed.

No framework/library it’s just kind of how I write my code for most of my projects I do think I’ll need to look into other setups though or improve mine.

haha this is just the result of quick sloppy work for testing I didn’t feel like setting up UI or anything since I don’t have UI code yet so I opted to just use a single NPC with a proximity prompt I suppose I could make use of collection service, but not sure if I’ll keep this even if I did it would probably just be the one npc. Mainly just wanted a quick way for the client to be able to select a character.

Anyways thanks for the response much appreciated.

Here’s how you can set up a simple character selection:

-- LocalScript (place in StarterGui)

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local CharacterSelectionEvent = ReplicatedStorage:WaitForChild("CharacterSelection")

local ScreenGui = script.Parent
local CharacterButtons = ScreenGui:WaitForChild("CharacterButtons")

local function onCharacterSelected(characterName)
    -- Send the selected character to the server
    CharacterSelectionEvent:FireServer(characterName)
end

-- Connect the ImageButton.MouseButton1Click event for each character button
for _, button in pairs(CharacterButtons:GetChildren()) do
    if button:IsA("ImageButton") then
        button.MouseButton1Click:Connect(function()
            onCharacterSelected(button.Name)
        end)
    end
end

-- Script (place in ServerScriptService)

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local CharacterSelectionEvent = ReplicatedStorage:WaitForChild("CharacterSelection")

local function onCharacterSelected(player, characterName)
    -- Apply the selected character to the player
    -- This will depend on how your game is set up and how you store and apply characters
end

-- Listen for character selection events from clients
CharacterSelectionEvent.OnServerEvent:Connect(onCharacterSelected)

It’d be better to just keep that data on the client at all times. I got three reasons for this:

  • You’re loading additional copies of the same module every time that character is needed. If you’re instead requiring from the same module, you end up with the same singular reference of that module that you can use from anywhere. The way you’re currently doing things could potentially backfire and lead to a memory leak.

  • Roblox makes some attempts at minimizing duplicate modules. If it works in relation to the rest of your setup, it will undo whatever optimization you’re trying to achieve by basically giving you the same exact behavior as if you just never copied the modules to begin with.

  • Over the past couple years working as a dev I’ve only personally ran into a situation where I ran out of memory twice, and both of those times the culprit ended up being the map on low end mobile devices rather than scripts.

Your modules really won’t be impacting memory all that much unless you have an entire AI language model saved in there. And even if your modules are loaded in, they won’t actually have any tangible impact on your game’s performance until they actually start being used.

1 Like

This idea may not work well but you could use Object-Orientated-Programming and attributes!

  • You could create an attribute for the player which represents the players class.

  • Then you could detect when the attribute is changed and find a module script that is the name of the players class. If it’s found, the first things you would do is check if player has a current class and clean it up!

  • Next you can create a new class from the module and give it some parameters like the player and some remote events!

Here are two scripts to show this idea:

ServerScript that handles the player attributes and switching classes!

-- [Variables] --
local Players = game:GetService("Players")
local Characters = CharacterFolderPath

-- [Script] --
local function OnPlayerAdded(Player:Player)
	
	local CurrentCharacterModule = nil
	Player:SetAttribute("Class", "NoClass")
	
	Player:GetAttributeChangedSignal("Class"):Connect(function()
		local NewClass = Player:GetAttribute("Class")
		
		if (CurrentCharacterModule ~= nil) then
			CurrentCharacterModule:Destroy()
		end
		
		local CharacterModule = Characters:FindFirstChild(NewClass)
		
		if (CharacterModule ~= nil) then
			-- You can give it any paramaters you need like some stats or remote events!
			CurrentCharacterModule = require(CharacterModule).New(Player)
		end
	end)
end

for _, Player in ipairs(Players:GetPlayers()) do
	task.spawn(OnPlayerAdded, Player)
end

Players.PlayerAdded:Connect(OnPlayerAdded)

ModuleScript that would be the class of the player(For the server)!

-- [Variables] --

-- [CharacterCls] --
local FireElemental = {}
FireElemental.__index = FireElemental

function FireElemental.New(Player)
	local self = {}
	setmetatable(self, FireElemental)
	
	self.Armor = Instance.new("Part")
	self.Player = Player
	
	return self
end

function FireElemental:Destroy()
	self.Armor:Destroy()
end

return FireElemental

You could also apply this to the client for things like input detecting and local effects!

This may not work but if I made a game with a character selection system I would do this!

Any questions are appreciated! :yum::yum::yum: