Skip to content

A character creation screen

A player’s look is four parts of the game’s characters: the skin, the face, the hair with its color and the beard. The server owns it - the mode sets it with SetPlayerLook, everyone sees it, the player’s own game too - and the mode decides how a player gets to choose one. A character creation screen is the mode’s own, in two halves:

  • the server half (the game mode) sends the parts it offers, opens the screen, takes the look the player settles on in OnPlayerLookChange and saves it;
  • the client half (a client script the server sends with the mode) draws the menu, shows every change on a preview body in front of the player that nobody else sees (ShowLookPreview), and asks for the look (RequestLookChange).

The shipped BasicRP mode has a complete one: an account’s first login without a look goes through it - in a virtual world of the player’s own at the spawn, so nobody sees a half-made character - and into the shared world when it is done; /look opens it again any time, and the look is saved with the account. Its files are the worked example of this page - gamemodes/basicrp.lua and gamemodes/basicrp/client/creator.lua in the server folder; basicrp/client/account.lua next to it is the password box a player registers and logs in with (a text field of the script’s own: OnTextInput, and QuitGame for its Exit).

GetLookParts(kind [, pattern]) lists what a man’s look may name, in the game’s order, for "head" (the face), "hair", "beard" and "body" (the skin); the look parts are the same lists on paper. Each entry has its name, its style (what it derives from), its traits (the words the game matches faces, skins and hair by: Pale, Suntanned, Posh, YoungAdult …) and generic - true for the plain parts the game gives its crowds, not a named character’s, not a wound’s.

A few rules make good lists:

  • faces: the generic heads whose style is "" - the heads, not their variants;

  • hair: a hair model (x_hair_05) derives its colors (m_hair_005_black, m_hair_005_blonde …) - offer the model as a style and its generic colors as a second row; x_hair_20 is a shaved head, m_hair_barber_01 to _16 the barber’s cuts;

  • beards: m_beard_00 is none, the generic m_beard_*, and UC_beard_barber_01 to _08 the barber’s;

  • skins: the generic m_body_* tones (the pale, the tanned and the dark ones, young and old).

  • a woman: GetLookParts(kind, pattern, "female") lists her faces, hair and skins (the generic f_body_* and f_roma_* tones; she has no beard). Send both lists and let the player pick the body first; the look the creator asks for says gender = "female". The preview turns into a woman’s body with it, and once the mode takes the look the player’s own game asks them to restart on her body - it loads with the level - and remembers it for the next join. She can do everything he can: the single-player game’s rules that keep women off horses, grindstones and alchemy tables do not apply to players, so a mode needs nothing extra for her to ride, sharpen or brew.

local creating = {} -- pid -> true while the creator is open
function openCreator(pid, first)
creating[pid] = true
SendClientEvent(pid, "creator_parts", partsLine) -- the lists, once (a plain string of your own format)
SendClientEvent(pid, "creator", (first and "new;" or "edit;") .. LookToString(GetPlayerLook(pid)))
end
-- the player's "done": asked only after the server checked every part
function OnPlayerLookChange(pid, look)
if not creating[pid] then return false end -- no creator open: no look from this player
creating[pid] = nil
db:Execute("UPDATE players SET look = @l WHERE name = @n", { l = LookToString(look), n = GetPlayerName(pid):lower() })
SendClientEvent(pid, "creator", "close")
return true -- the look is the player's, on every screen
end

OnPlayerLookChange is the only door: a mode without it takes no look from a player, so a player cannot give themselves a look on a server that offers no way to. The server has already checked the parts (a part of that kind, all of them a man’s or all a woman’s; one ask a second); the mode checks what it cares about - here, that its creator is open.

The look travels as one line of text, "body=m_body_tan_02;head=m_head_004;hair=m_hair_005_blonde;beard=m_beard_03" (LookToString; a woman’s starts with gender=female;), which fits a TEXT column. At the next visit it goes straight back:

-- at the login, with the row read from the database
if not (row.look and row.look ~= "" and SetPlayerLook(pid, row.look)) then
openCreator(pid, true) -- no look yet (or a part a game update removed): make one
end

The server itself remembers no look past a visit: without the mode’s database a look lasts until the player leaves.

A file in the mode’s client/ folder (gamemodes/<mode>/client/*.lua, or gamemodes/<mode>/client/ next to a single-file gamemodes/<mode>.lua); the server sends it to every player who joins.

local parts, look, row = nil, {}, 1
function OnServerEvent(name, payload)
if name == "creator_parts" then parts = parseParts(payload) end
if name == "creator" then
if payload == "close" then return close() end
local kind, line = payload:match("^(%a+);(.*)$")
look = LookFromString(line) -- the player's look to start from ({} for a new character)
SetKeyboardCapture(true) -- the menu has the keys; the player stands while choosing
drawMenu() -- CreateUiClip / CreateUiRect / CreateUiText under one clip
ShowLookPreview(look, {distance = 1.6}) -- a body in front of the player, dressed as they are
end
end
function OnKey(key, pressed)
if not pressed then return end
if key == "d" then look.hair = nextHair(look.hair) ShowLookPreview(look) redrawMenu() end
if key == "q" then ShowLookPreview(look, {turn = -30}) end
if key == "space" then RequestLookChange(look) end -- the mode's OnPlayerLookChange decides
end
function close()
HideLookPreview()
DestroyUiElement("menu")
SetKeyboardCapture(false)
end

What each piece does:

  • SetKeyboardCapture(true) gives every key to OnKey and the game takes no input meanwhile - the player neither walks nor turns away from the preview, Enter does not open the chat, Esc is yours to close the menu with. It ends with false, a reload of the scripts or the end of the session; a script whose OnKey fails gives it back by itself.
  • ShowLookPreview(look, options) makes the body the first time - where the player looks, distance metres ahead on their floor, facing them - and changes it in place afterwards: the look, the distance (0.9 is the face, 2.5 the whole body), the turn in degrees. Only this player sees it.
  • RequestLookChange(look) asks the server; when the mode takes the look, OnLookChange tells the script, and GetPlayerLook answers with it from then on. A mode that confirms with a script event of its own (“close” above) also covers the player who kept the look they had.

The preview body is placed where the player looks when the creator opens. Open it where there is room in front of them - the spawn of a mode is usually such a place; a mode that wants a set stage moves the player first (SetPlayerPos with a yaw that faces the open side) and opens the creator a moment later. A player who should not be seen while choosing goes into a virtual world of their own and back into 0 when done.

  • The server has the tables export ([combat] tables in server.toml): without it GetLookParts is empty and the parts pass unchecked - the tables guide makes the file.
  • The mode defines OnPlayerLookChange and takes a look only while its creator is open.
  • The look is saved as LookToString’s line and given back with SetPlayerLook at the login.
  • The client half gives the keyboard back and hides the preview on every way out: done, cancel, the mode’s close.