,
I am well versed in multiple aspects of game developments through many small solo projects.
My field of expertise:
- 3D Assets; 6/10
- Blender being the primary software I use, alongside with Substance Painter.
- Creativity is not my forte so modelling isn’t really what I can do without any creative directive.
- Experience: 3+ years.
- Programming; 9.5/10
- Fluent with Lua, Python, Javascript.
- Extremely well with both back-end and front-end stuff, i.e data handling, databases etc.
- Have experiences on different programming paradigms with different concepts such as composition and inheritance. (Mostly OOP)
- Created basic full stack web applications with NodeJS.
- Experience: 4+ years.
- Image manipulation; 7/10
- Very fluent with
Paint.net
. - Been using it to create simple user interfaces.
- Lack the ability to draw though.
- Very fluent with
- Animation; 4/10
- Have the ability to set up rigs within Blender, with knowledge on armatures and weight painting.
- Rather good understanding of the fundamentals.
- Such as keyframing, frames interpolation etc.
- Roblox Studio; 9/10
- Been on it for almost 3 years now.
- Vast knowledge of this game engine.
Models:
Low poly styled:
- food objects
- low poly styled stage light
- staff
- cannongun
- pumps taking the shape of traditional Japan buildings
- self ordering kiosk
- classic pine tree
- flamethrower design A
- flamethrower design B
- japanese-styled lantern
High poly; Renders:
- cola can render; can’s texture was obtained from the internet, with materials done within Blender
- headset render, modelled and rendered all in Blender
- medieval helmet
- trash can with car-like wheels
- renders of the Lamy Safari Fountain Pen; final model had some variation in terms of physical shape
UGC Concepts:
Codes:
What I am able to do:
- Datastores; both the Roblox Datastore and external datastores interacted with the REST API such as Firebase by Google (with authentication workflows implemented such as OAuth2).
- Tween service.
- CFrames; highly experienced with this. I’ve had many projects that needed heavy CFrames to achieve the intended outcome.
- Modulescripts for modular systems.
- OOP, metatables and metamethods etc.
- Interfaces. Maintaining good infrastructure of UI elements.
- Raycasting, segmented raycsating (recursive).
- Bodymovers. Only experience was creating spaceships.
- UserInputService (UIS)
- ContextActionService (CAS)
What I am unable to do:
- Custom characters.
Model Showcase system
Made this as an effort to showcase my friend’s amazing models.
I’ve used,
- tweenservice,
- userinputservice
- runservice
- viewportframes
to achieve it.
Featuring a free camera movement to view the models from all angles,
function setCamCFrame(cam, object, x, y, offset)
local camAng = cam.CFrame
local camAngX, camAngY, camAngZ = camAng:ToEulerAnglesYXZ()
local angleX = math.deg(camAngX) -y
if angleX > 65 then
angleX = 65
end
if angleX < -65 then
angleX = -65
end
camAng = CFrame.fromOrientation(math.rad(angleX), camAngY +math.rad(x), 0)
cam.CFrame = (object.PrimaryPart.CFrame * camAng * CFrame.new(0, 0, offset))
end
Short Showcase:
Search System:
Search system uses string.find()
; nothing significant
Game:
Mirroring GUI
Mirrors the GUI Frame along with its descendants and reflect it onto a new screen
Works for both axis
Nothing of any significance, just ought I left it here.
local frame = script.Parent:WaitForChild("Frame")
local function mirrorDescendants(frameParam, vertical)
local newFrame = frameParam:Clone()
newFrame.Parent = script.Parent
if vertical then
for _, children in pairs(newFrame:GetDescendants()) do
if children:IsA("GuiObject") then
local X, Y = 1 - children.AnchorPoint.X, 1 - children.AnchorPoint.Y
children.AnchorPoint = Vector2.new(children.AnchorPoint.X, Y)
local pos = children.Position
children.Position = UDim2.new(pos.X.Scale, 0, 1 - pos.Y.Scale, 0)
end
end
else
for _, children in pairs(newFrame:GetDescendants()) do
if children:IsA("GuiObject") then
local X, Y = 1 - children.AnchorPoint.X, 1 - children.AnchorPoint.Y
children.AnchorPoint = Vector2.new(X, children.AnchorPoint.Y)
local pos = children.Position
children.Position = UDim2.new(1- pos.X.Scale, 0, pos.Y.Scale, 0)
end
end
end
return newFrame
end
local mirroredFrame = mirrorDescendants(frame, true)
mirroredFrame.Position = UDim2.new(0.3, 0, 0.7, 0)
local mirroredFrame = mirrorDescendants(frame, false)
mirroredFrame.Position = UDim2.new(0.7, 0, 0.3, 0)
Horizontal and vertical mirroring
Studio Plugin
A plugin that shows the currently selected(or previously selected if no parts are selected) part’s size.
It features a string formatting system that gives each strings a set amount of width
This prevents the individual values from shifting from their position without the need of 3 different textlabels.
Helps with readability
local emptySpace = " "
local function format(str)
str = tostring(str)
if string.len(str) < 10 then
local emptySpaceNeeded = string.len(str) - 8
local emptySpace = string.sub(emptySpace, emptySpaceNeeded)
local newStringWithEmptySpace = str..emptySpace
return newStringWithEmptySpace
end
return str
end
Code responsible for getting the selection and size.
local function GetSize(obj, gui)
local x, y, z = obj.Size.X, obj.Size.Y, obj.Size.Z
local list = {x, y, z}
for i, v in ipairs(list) do
v = round(v)
list[i] = v
end
local str = finalFormat(list)
gui = gui.TextLabel
gui.Text = str
end
local function getObject(gui)
local obj = Selection:Get()[#Selection:Get()]
if not (obj:IsA('BasePart')) or obj == nil then
return
end
GetSize(obj, gui)
return obj
end
local event = nil
Selection.SelectionChanged:Connect(function()
currentlySelect = getObject(gui)
if currentlySelect ~= nil then
if event ~= nil then event:Disconnect() end
event = currentlySelect:GetPropertyChangedSignal('Size'):Connect(function()
GetSize(currentlySelect, gui)
end)
end
end)
Generic Datastore
A functional datastore backend.
Roblox datastore’s wrapper?
Uses proxy tables as metatables to detect when a value changes.
__newindex
metamethod tanking the whole feature of value change detection
local backend = {}
local ds = game:GetService("DataStoreService"):GetDataStore("testb")
local rs = game:GetService("ReplicatedStorage")
-- constants
local RETRYLIMIT = 5
-- default values for new players
local defaultData = {
time_played = 0
}
-- to notify player if a value change occurred
local hookEvent = Instance.new("RemoteEvent", rs:WaitForChild("datastoreObjects"))
local hooks = {} -- store player instance if plr wants to be notified
local serverHooks = {} -- for server hook
local cache = {} -- read
local proxy = {} -- write; contents of proxy will never be written
-- proxy[key] = value will transfer towards cache table, cache[key] = value
-- to allow __newindex to trigger as luau and lua have different behaviours
local function transformDictKeysToString(dict)
-- recursive function
local re = {}
for k, v in pairs(dict) do
if type(k) == "number" then
k = tostring(k)
end
if type(v) == "table" then
v = transformDictKeysToString(v)
end
re[k] = v
end
return re
end
setmetatable(proxy, {
__newindex = function(a, plr, v)
print('change detected')
cache[plr] = v
if v == nil then
return
end
local send = transformDictKeysToString(v) -- to enable dictionaries to be sent through remotes/bindables
if hooks[plr] then
hookEvent:FireClient(plr, v)
end
for _, serverHookEvent in pairs(serverHooks) do
print('firing')
serverHookEvent:Fire(plr, v)
end
end})
function backend.getData(plr)
-- retrieves data
local plrData = nil
if cache[plr] ~= nil then
plrData = cache[plr]
else
for i = 1, RETRYLIMIT do
local s, e = pcall(function()
plrData = ds:GetAsync(plr.UserId)
end)
if s then
break
elseif i == RETRYLIMIT then
warn("could not call :GetAsync successfully, with error, "..tostring(e))
return {false, e}
end
end
if plrData == nil then
plrData = defaultData
for i = 1, RETRYLIMIT do
local s, e = pcall(function()
ds:SetAsync(plr.UserId, plrData)
end)
if s then
break
elseif i == RETRYLIMIT then
warn("Could not call :SetAsync successfully, with error: "..tostring(e))
end
end
end
proxy[plr] = plrData
end
return {true, plrData}
end
function backend.pushData(plr)
-- pushes cached data into datastore
local plrData = backend.getData(plr)
if not plrData[1] or plrData[2] == nil then
-- SHOULD NOT HAPPEN AS GETASYNC WAS CALLED TO SET CACHE VALUE
warn("Could not retrieve player data to push to datastore with error: "..tostring(plrData[2]))
plrData = cache[plr] -- as an last ditch effort
if plrData == nil then
error("Could not push data to datastore as plrData is nil")
return {false, "plrData is nil"}
end
end
for i = 1, RETRYLIMIT do
local s, e = pcall(function()
ds:UpdateAsync(plr.UserId, function()
return plrData[2]
end)
end)
if s then
break
elseif i == RETRYLIMIT then
error("Could not push data to datastore as :UpdateAsync could not be called with error: "..tostring(e))
return {false, e}
end
end
return {true}
end
function backend.setData(plr, newData)
-- updates the data stored with newData
proxy[plr] = newData
end
function backend.changeValueByKey(plr, k, v)
if cache[plr] == nil then
backend.getData(plr) -- init
end
local data = cache[plr]
data[k] = v
proxy[plr] = data
end
function backend.incrementValueByKey(plr, k, v)
if cache[plr] == nil then
backend.getData(plr) -- init
end
local data = cache[plr]
data[k] += v
proxy[plr] = data
end
function backend.getDefaultData()
return defaultData
end
function backend.removeReferences(plr)
-- called on PlayerRemoving event
if cache[plr] == nil then
-- player joined and leave immediately
else
backend.pushData(plr)
end
cache[plr] = nil
hooks[plr] = false
end
function backend.getHook(plr)
-- fires a remoteevent upon data changes
hooks[plr] = true
return hookEvent
end
function backend.dropHook(plr)
hooks[plr] = false
return true
end
function backend.newServerHook()
local newHook = Instance.new("BindableEvent", script)
newHook.Parent = rs:WaitForChild("datastoreObjects")
table.insert(serverHooks, newHook)
return newHook
end
return backend
On addition to this, another script is used as a proxy to interact with the module script.
I.e:
local plrs = game:GetService("Players")
local rs = game:GetService("ReplicatedStorage")
local remoteFunc = rs:WaitForChild("Remotes"):WaitForChild("PetData")
local serverFunc = script:WaitForChild("serverConnection")
local ds = require(script:WaitForChild("datastore"))
local tasks = {
function(plr) -- get data
return ds.getData(plr)
end,
function(plr) -- get hook
return ds.getHook(plr)
end,
function(plr) -- drop hook
return ds.dropHook(plr)
end
}
local clientAllowed = {1, 2, 3}
remoteFunc.OnServerInvoke = function(plr, actioncode, ...)
local passed = false
for _, i in pairs(clientAllowed) do
if actioncode == i then
passed = true
break
end
end
if not passed then
-- not permitted to be executed on the client
return {false}
end
return tasks[actioncode](plr, ...)
end
serverFunc.OnInvoke = function(plr, actioncode, ...)
return tasks[actioncode](plr, ...)
end
plrs.PlayerRemoving:Connect(function(plr)
ds.removeReferences(plr) -- saves data too
end)
Furniture Place System
A basic furniture placement system that features rotations too.
Rather secure in terms of exploitability.
Uses OOP.
Steps
- User selects what objects they want to place
CAS:BindAction('A', getObject, true, Enum.KeyCode.Q)
CAS:BindAction('B', getObject, true, Enum.KeyCode.E)
-
All of the placing is done on the client side, only replicated to the server when it passes checks.
-
Gets user’s mouse, clamp the value to ensure parts are within the canvas
local x = math.clamp(mouseX, self.gridZeroX, self.finalGridX)
local z = math.clamp(mouseZ, self.gridZeroZ, self.finalGridZ)
local ht = objHeight/2 + canvasPos.Y + canvasSize.Y/2
- Gets the nearest grid’s position
local xA = math.floor((x - self.gridZeroX)/gridSpace +0.5)
local zA = math.floor((z - self.gridZeroZ)/gridSpace +0.5)
x = self.gridZeroX + gridSpace*xA
z = self.gridZeroZ + gridSpace*zA
- Verify if object is not colliding with others
local function verifyPlace(objClass)
local allowedtoplace = true
local model = objClass.Object
local modelPos = model.PrimaryPart.Position
local modelSize = model.PrimaryPart.CFrame:VectorToWorldSpace(model.PrimaryPart.Size)
for _, v in pairs(workspace.ignoredObjects:GetChildren()) do
if v == model then continue end
local vPos = v.PrimaryPart.Position
local vSize = v.PrimaryPart.CFrame:VectorToWorldSpace(v.PrimaryPart.Size)
local xFactor = math.max(vPos.X, modelPos.X) - math.min(vPos.X, modelPos.X)
local xSize = math.abs(vSize.X)/2 + math.abs(modelSize.X)/2
local zFactor = math.max(vPos.Z, modelPos.Z) - math.min(vPos.Z, modelPos.Z)
local zSize = math.abs(vSize.Z)/2 + math.abs(modelSize.Z)/2
if xFactor < xSize and zFactor < zSize then
allowedtoplace = false
break
end
end
changetoRed(model, not allowedtoplace)
return allowedtoplace
end
function placement:PlaceVerify()
local canPlace = verifyPlace(self)
return canPlace
end
Constructed my own object of the class for every object.
This features,
- Dynamic sizes,
- Rotation,
- Cancelling placement (just calling
:Destroy()
on the object), - Canvas lock, only allowing parts to be placed within the canvas,
- Server sided checks, to ensure nothing is not where is shouldn’t be
Showcase:
Stacking Blocks
Classic stacking game.
Its all purely parts, no CSG or Unions were involved.
Made possible with CFrames .
Data saving along with leaderboard
- Achieved it using
OrderedDataStore
- Refreshes approx. every 30minutes in game, intervals are influenced by the current DSS budget.
Showcase:
Stacking Game Showcase - YouTube
Discontinued work on it.
Purchase Listeners
Monitors PurchasePrompts
using the PromptPurchaseFinished service.
Along with RemoteEvents and :SetCore()
function.
Loading Screen
A simple loading screen made using TweenService
.
Main part was just choreographing everything.
Code:
-- disabling core guis
local startergui = game:GetService("StarterGui")
local UIS = game:GetService("UserInputService")
startergui:SetCoreGuiEnabled(Enum.CoreGuiType.All, false)
UIS.ModalEnabled = true
--
local contentprovider = game:GetService("ContentProvider")
local plr = game:GetService("Players").LocalPlayer
local ts = game:GetService("TweenService")
local fFrame = script.Parent:WaitForChild("first")
local sFrame = script.Parent:WaitForChild("second")
local touch = fFrame:WaitForChild("touch")
local initialMsg = fFrame:WaitForChild("init")
local promptMsg = fFrame:WaitForChild("prompt")
local fText = fFrame:WaitForChild("header")
local fImg = fFrame:WaitForChild("ImageLabel")
local sImg = sFrame:WaitForChild("ImageLabel")
local sBlinds = sImg:WaitForChild("blinds")
local sWhite = sFrame:WaitForChild("blank")
local LIMIT = 30 -- seconds
local function loading() -- yield control of current script until everything has loaded in
local loaded = true
local start = tick()
contentprovider:PreloadAsync({fImg.Image, sImg.Image})
while true do -- primitive check conditions for fully loaded
if not fImg.IsLoaded then
loaded = false
elseif not sImg.IsLoaded then
loaded = false
elseif plr.Character == nil then
loaded = false
end
if loaded or tick() - start > LIMIT then
break
end
wait()
end
return false
end
local isLoading = true
coroutine.wrap(function()
while isLoading do
local suffix = ""
for i = 1, 3 do
suffix = suffix.."."
initialMsg.Text = "Loading"..suffix
wait(0.3)
end
end
end)()
local isLoading = loading() -- yield code
print("LOADING FINISHED")
initialMsg.Visible = false
local promptText_TI = TweenInfo.new(
2,
Enum.EasingStyle.Linear,
Enum.EasingDirection.InOut,
-1,
true
)
local promptText_T = ts:Create(promptMsg, promptText_TI, {TextTransparency = 1})
local function tweenHandler(tweenObj, i, t)
local tween = ts:Create(tweenObj, i, t)
tween:Play()
tween.Completed:Wait()
end
local fired = false -- simple debounce
touch.MouseButton1Click:Connect(function()
-- disabling touch event
if fired then
return -- debounce
end
fired = true
touch.Visible = false
touch.Active = false
-- hide prompt text
promptMsg.Visible = false
promptText_T:Cancel()
-- fade in first background
tweenHandler(fFrame, TweenInfo.new(1), {BackgroundColor3 = Color3.fromRGB(30, 30, 30)})
-- fade in text
local f_TI = TweenInfo.new(
2,
Enum.EasingStyle.Sine,
Enum.EasingDirection.Out,
0
)
fText.Visible = true -- show text
fImg.Visible = true -- show image
tweenHandler(fText, f_TI, {Position = UDim2.new(0.5, 0, 0.3, 0)})
-- fade in image
tweenHandler(fImg, f_TI, {Position = UDim2.new(0.5, 0, 0.5, 0)})
wait(3)
-- moving onto second frame
local s_TI = TweenInfo.new(
1.2,
Enum.EasingStyle.Linear,
Enum.EasingDirection.InOut,
0
)
-- pull down second frame's container
sFrame.Visible = true
tweenHandler(sFrame, TweenInfo.new(2.5, Enum.EasingStyle.Bounce, Enum.EasingDirection.Out, 0), {Position = UDim2.new(0.5, 0, 0, 0)})
sImg.Visible = true -- show image icon
-- pull down img's blind in second frame
tweenHandler(sBlinds, f_TI, {Position = UDim2.new(0.5, 0, 1, 0)})
wait(3.5)
-- flashbang!!
sWhite.Visible = true
tweenHandler(sWhite, s_TI, {BackgroundTransparency = 0})
-- hide first slide
fFrame.Visible = false
-- hide purple bg and img for second slide
sFrame.BackgroundTransparency = 1
sImg.Visible = false
-- un-flashbang!!
tweenHandler(sWhite, s_TI, {BackgroundTransparency = 1})
wait(0.5)
startergui:SetCoreGuiEnabled(Enum.CoreGuiType.All, true)
UIS.ModalEnabled = false
end)
initialMsg.Visible = false -- hide loading msg
-- enable click event
touch.Visible = true
touch.Active = true
promptMsg.Visible = true
promptText_T:Play()
Showcase:
Event Class
A class that can send signals and fire functions between scripts.
Acts exactly like a BindableEvent
in terms of how they work.
OOP at it’s finest B)
Code:
local signalHandler = {}
local signalClass = {}
signalClass.__index = signalClass
-- :Connect()'s and :Disconnect()'s events
local signalSiblingClass = {}
signalSiblingClass.__index = signalSiblingClass
function signalHandler.new()
local newSignal = {}
setmetatable(newSignal, signalClass)
-- stores functions to be callled later on
newSignal.fList = {}
-- stores coroutine objects for :Wait()
newSignal.tList = {}
return newSignal
end
function signalClass:Connect(f)
-- f being the transform function
local newSiblingClass = {}
setmetatable(newSiblingClass, signalSiblingClass)
newSiblingClass.parent = self
newSiblingClass.func = f
self.fList[newSiblingClass] = true
return newSiblingClass
end
function signalSiblingClass:Disconnect()
-- self.parent is signalClass
self.parent.fList[self] = nil
setmetatable(self, nil)
self.parent = nil
end
function signalClass:Wait()
table.insert(self.tList, coroutine.running())
return coroutine.yield()
end
function signalClass:Fire(...)
for siblingclass in pairs(self.fList) do
siblingclass.func(...)
end
-- fire threads
for _, y in pairs(self.tList) do
coroutine.resume(y, ...)
end
self.tList = {}
end
function signalClass:Destroy()
for f in pairs(self.fList) do
setmetatable(f, nil)
end
self.fList = {}
setmetatable(self, nil)
end
return signalHandler
Demo usage code:
local signal = require(module)
local event = signal.new()
local connection = event:Connect(function(b)
print(b)
end)
event:Fire("Hello world!")
wait(5)
connection:Disconnect()
event:Destroy()
Ferris Wheel
Games:
Geisha
Description: a game under the horror genre with the main theme on Japanese urban legends
Programmed all of the game’s core function except for ‘Part I’ of the game.
Link: Geisha - Roblox
Timezone, GMT -8
Prices are negotiable; payment only with either Paypal or funds.
A down payment of 50% will be preferred.
I’ll appreciate every opportunity I get to work with others in hopes of diversifying my portfolio.
That said, I am up for any challenges.
Drop me a direct message on this site.