How to make a 2D game without UI

This tutorial will go over everything needed to make a 2D game without the use of any UI whilst still feeling like there is no third dimension, similar to paper in a way because paper takes up some space in the third dimension but it’s so thin that paper can seem 2D. This works when playing solo however I do not know if it works with multiple players. This tutorial is not perfect and has a few issues. Some players have fallen through the entire map however I have no idea what might be causing it.

Camera & Movement Scripts

First you will need a 2D camera script.
A script like the following should set the camera to be stuck in 1 position.

local camera = game.Workspace.CurrentCamera
local player = game.Players.LocalPlayer
local Character = player.Character or player.Character:Wait()
camera.CameraType = Enum.CameraType.Scriptable
camera.CameraSubject = player.Character.Head

local part = Instance.new("Part", player.Character.Torso)
part.Orientation = Vector3.new(0, 180, 0) --change
part.Name = "CameraPart"
part.Size = Vector3.new(1,1,1)
part.Anchored = true
part.CanCollide = false

game:GetService("RunService").Stepped:Connect(function()
	part.Position = Character.Torso.Position + Vector3.new(0, 3, -20) --3 makes it so the camera follows the player's head and -20 is the distance from the player the camera is.
	camera.CFrame = part.CFrame

end)

Note that this script should be a local script under StarterCharacterScripts.
Next get the PlayerModule scripts. This can be done by testing the game in Roblox Studio, going under StarterPlayerScripts, copying the entire PlayerModule, stop testing the game, and pasting the scripts into StarterPlayerScripts.
Next find the ControlModule under the PlayerModule. Next under the ControlModule find the Module Script called “Keyboard”. Then delete the parts that allow the player to move left and right. These are lines 100 - 111.
I have also provided that script without the left and right movement if you choose to just copy and paste it over the normal Keyboard script.

Keyboard Script Without Left & Right Movement
--!nonstrict
--[[
	Keyboard Character Control - This module handles controlling your avatar from a keyboard

	2018 PlayerScripts Update - AllYourBlox
--]]

--[[ Roblox Services ]]--
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")

--[[ Constants ]]--
local ZERO_VECTOR3 = Vector3.new(0,0,0)

--[[ The Module ]]--
local BaseCharacterController = require(script.Parent:WaitForChild("BaseCharacterController"))
local Keyboard = setmetatable({}, BaseCharacterController)
Keyboard.__index = Keyboard

function Keyboard.new(CONTROL_ACTION_PRIORITY)
	local self = setmetatable(BaseCharacterController.new() :: any, Keyboard)

	self.CONTROL_ACTION_PRIORITY = CONTROL_ACTION_PRIORITY

	self.textFocusReleasedConn = nil
	self.textFocusGainedConn = nil
	self.windowFocusReleasedConn = nil

	self.forwardValue  = 0
	self.backwardValue = 0
	self.leftValue = 0
	self.rightValue = 0

	self.jumpEnabled = true

	return self
end

function Keyboard:Enable(enable: boolean)
	if not UserInputService.KeyboardEnabled then
		return false
	end

	if enable == self.enabled then
		-- Module is already in the state being requested. True is returned here since the module will be in the state
		-- expected by the code that follows the Enable() call. This makes more sense than returning false to indicate
		-- no action was necessary. False indicates failure to be in requested/expected state.
		return true
	end

	self.forwardValue  = 0
	self.backwardValue = 0
	self.leftValue = 0
	self.rightValue = 0
	self.moveVector = ZERO_VECTOR3
	self.jumpRequested = false
	self:UpdateJump()

	if enable then
		self:BindContextActions()
		self:ConnectFocusEventListeners()
	else
		self:UnbindContextActions()
		self:DisconnectFocusEventListeners()
	end

	self.enabled = enable
	return true
end

function Keyboard:UpdateMovement(inputState)
	if inputState == Enum.UserInputState.Cancel then
		self.moveVector = ZERO_VECTOR3
	else
		self.moveVector = Vector3.new(self.leftValue + self.rightValue, 0, self.forwardValue + self.backwardValue)
	end
end

function Keyboard:UpdateJump()
	self.isJumping = self.jumpRequested
end

function Keyboard:BindContextActions()

	-- Note: In the previous version of this code, the movement values were not zeroed-out on UserInputState. Cancel, now they are,
	-- which fixes them from getting stuck on.
	-- We return ContextActionResult.Pass here for legacy reasons.
	-- Many games rely on gameProcessedEvent being false on UserInputService.InputBegan for these control actions.


	local handleMoveLeft = function(actionName, inputState, inputObject)
		self.leftValue = (inputState == Enum.UserInputState.Begin) and -1 or 0
		self:UpdateMovement(inputState)
		return Enum.ContextActionResult.Pass
	end

	local handleMoveRight = function(actionName, inputState, inputObject)
		self.rightValue = (inputState == Enum.UserInputState.Begin) and 1 or 0
		self:UpdateMovement(inputState)
		return Enum.ContextActionResult.Pass
	end

	local handleJumpAction = function(actionName, inputState, inputObject)
		self.jumpRequested = self.jumpEnabled and (inputState == Enum.UserInputState.Begin)
		self:UpdateJump()
		return Enum.ContextActionResult.Pass
	end

	-- TODO: Revert to KeyCode bindings so that in the future the abstraction layer from actual keys to
	-- movement direction is done in Lua
	ContextActionService:BindActionAtPriority("moveLeftAction", handleMoveLeft, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterLeft)
	ContextActionService:BindActionAtPriority("moveRightAction", handleMoveRight, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterRight)
	ContextActionService:BindActionAtPriority("jumpAction", handleJumpAction, false,
		self.CONTROL_ACTION_PRIORITY, Enum.PlayerActions.CharacterJump)
end

function Keyboard:UnbindContextActions()
	ContextActionService:UnbindAction("moveForwardAction")
	ContextActionService:UnbindAction("moveBackwardAction")
	ContextActionService:UnbindAction("moveLeftAction")
	ContextActionService:UnbindAction("moveRightAction")
	ContextActionService:UnbindAction("jumpAction")
end

function Keyboard:ConnectFocusEventListeners()
	local function onFocusReleased()
		self.moveVector = ZERO_VECTOR3
		self.forwardValue  = 0
		self.backwardValue = 0
		self.leftValue = 0
		self.rightValue = 0
		self.jumpRequested = false
		self:UpdateJump()
	end

	local function onTextFocusGained(textboxFocused)
		self.jumpRequested = false
		self:UpdateJump()
	end

	self.textFocusReleasedConn = UserInputService.TextBoxFocusReleased:Connect(onFocusReleased)
	self.textFocusGainedConn = UserInputService.TextBoxFocused:Connect(onTextFocusGained)
	self.windowFocusReleasedConn = UserInputService.WindowFocused:Connect(onFocusReleased)
end

function Keyboard:DisconnectFocusEventListeners()
	if self.textFocusReleasedConn then
		self.textFocusReleasedConn:Disconnect()
		self.textFocusReleasedConn = nil
	end
	if self.textFocusGainedConn then
		self.textFocusGainedConn:Disconnect()
		self.textFocusGainedConn = nil
	end
	if self.windowFocusReleasedConn then
		self.windowFocusReleasedConn:Disconnect()
		self.windowFocusReleasedConn = nil
	end
end

return Keyboard

Building

Now you will need an area for the player to move around in. First create a background. I made my background 1 stud thick and 2,000 studs in the other directions to make a giant square. Next you may center the part by setting it’s position to (0,0,0). Now move the background 5,000+ studs up. This is so that if you add on other background parts on the side (Maximum part size 2048) then it won’t go into the void. Make sure to anchor your background part! Now you can adjust the background to however you like. Personally I made my background’s material Neon and made it’s color (100,100,100). You may lock the background part to make it easier on yourself and name the background part something like “Background”.

Now create a part and put it somewhere near the center of the background. Make sure to anchor the part as well. Next make the part 0.1 studs or less thick. This is to stop ambient occlusion and to help with the 2D effect. Scale the part to your liking however keep it at most 0.1 studs thick. Make sure the part is still up against the background. You may change the part’s material and color to your liking however I made all of mine Smooth Plastic and their color (110,110,110). I make all of the parts like this except for a few because I’m making a silhouette effect.

Now that you have made your base part you need to make a part for the player to actually walk on. This is very simple to do. First copy the part you just made and paste it into workspace at it’s original location. Then scale it however many studs out until it is 3 studs thick (2.9 studs if the original part was 0.1 studs thick). Then set the part’s transparency to 1. Now you have created an area for the player to walk on.

You should create a SpawnLocation next to those parts you just made. Then set the SpawnLocation’s transparency to 1 and it’s decal’s transparency to 1. Next make the spawn location 1 stud in depth or more and 3 studs in the other directions. Now you may add a Base Part like the one before to match the SpawnLocation’s size. You may scale the part’s depth more to match the other Base Part. I’m calling the 0.1 stud thick parts that you can actually see Base Parts and the transparent parts Collision Parts.

Now you may add a Base Part and a Collision Part on the side of the other to make a wall. Now that you have that you should add another Collision Part. This Collision Part should be 1 stud thick or more. Now place this Collision part on the side furthest from the background of the wall Collision Part. Now make this Collision Part only overlap 1 stud onto the other one. Now make it over lap 1 stud onto the floor Collision Part. Then scale it to match the length of the floor Collision Part. Now you have a full area for the player to move in.

StarterCharacter

Now you must create a StarterCharacter. You can either find a blank StarterCharacter from the creator marketplace or use a plugin such as Character Inserter. I would make the StarterCharacter R6 because that’s what works at least for me. Now make everything on the StarterCharacter have a transparency of 1. Now find a white decal on the creator marketplace and put it on every single side of each part of the StarterCharacter. Then change the decal’s color to your likings. This is to stop ambient occlusion.

Now name your StarterCharacter the name “StarterCharacter” if not already. Now put the StarterCharacter under StarterPlayer. Make sure your StarterCharacter is unanchored. Now go under Game Settings. Next find the tab called “Avatar” Then set the Avatar Type to R6 (If you chose to make your StarterCharacter R6 otherwise choose R15). Now hit the save button.

Lighting

Set your Lighting properties to match these properties below.

Ambient: [255,255,255]
Brightness: 0
ColorShift_Bottom: [0,0,0]
ColorShift_Top: [0,0,0]
EnviromentDiffuseScale: 1
EnviromentSpecularScale: 1
GlobalShadows: False
OutdoorAmbient: [255,255,255]
ShadowSoftness: 0.2
Technology: ShadowMap
Archivable: True
ClockTime: 0
GeographicLatitude: 0
TimeOfDay: 00:00:00
ExposureCompensation: 0.5
UseCurrentLighting: False

Set your Atmosphere properties to match the properties below.
Density: 0
Offset: 0
Glare: 0
Haze: 0
Set your SunRay property Enabled to False as well.

Some things here may be adjusted but this helps with the 2D effect. In order to change the color of everything you may add ColorCorrection and use scripts like the following to change is as needed.

ColorCorrection Change Script
local ColorCorrection = game.Lighting.ColorCorrection
ColorCorrection.Parent = game.Lighting
local Part = script.Parent
local ChangeTime = false

local function ColorChange()
	if ChangeTime == false then
		ChangeTime = true
		ColorCorrection.TintColor = Color3.fromRGB(0,190,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,180,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,170,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,160,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,150,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,140,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,130,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,120,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,110,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,100,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,90,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,80,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,70,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,60,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,50,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,40,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,30,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,20,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,10,100)
		task.wait(0.1)
		ColorCorrection.TintColor = Color3.fromRGB(0,0,100)
	end
end

Part.Touched:Connect(ColorChange)

The Final Result

The final result will vary from person to person however the following screenshots show what my game looks like.

This concludes the tutorial on how to make a 2D game without UI. If you have any suggestions or comments then you may reply to this topic. Many things could have been better about this however I am inexperienced so the more experienced of people may find some things that somebody could do to improve on this.

If you want to play my game yourself and see how it turned out I will leave a link to it. The game is called Hypnagogia.

30 Likes

And color to it it to add some color to it?

4 Likes

What do you mean by that? I can’t tell.

4 Likes

I think he means to add a different color than the grey for the tint.

3 Likes

This is really inefficient and hard to read, you should use loops or TweenService for this

Whats the point of having a part for this? Why dont you just directly set the camera position?

This can be cleaned up into:

local camera = game.Workspace.CurrentCamera
local player = game.Players.LocalPlayer
local Character = player.Character or player.Character:Wait()
camera.CameraType = Enum.CameraType.Scriptable

game:GetService("RunService").Stepped:Connect(function()
	local Position = Character.Torso.Position + Vector3.new(0, 3, -20)
	camera.CFrame = CFrame.new(Position)
end)
5 Likes

As I said I do not have much experience with all of this and just made a tutorial that uses everything that I used to make my game. A few of the scripts I used were from people helping me out on the developer forum.

3 Likes

I understand but you shouldn’t be making a tutorial on something that you don’t have experience in, so I was trying to help fix up some of your code

3 Likes

There wasn’t a tutorial already explaining something like this so I decided to make one. Even if I could have made it better it still works. For some people this could really help. If I know that something works and I can explain how to do it the way that I did it, even if the way I did it isn’t even close to the best way, I’ll still make a tutorial on it. It took me many months to find out the exact right combination of everything and the right scripts in order to make the game I have now. If something like this already existed then it could have taken me a lot less time.

If I’m going to make a tutorial and it includes a script that I could make 2 different ways, one long but simple and one short but complicated one, but can only include one of the scripts then I would include the longer more simple script because to a person with less experience it would be easier to understand and they might be able to learn a thing or 2 off of it. If I just game somebody a complicated script even though they’re inexperience they might be able to use it but they will likely have trouble replicating something like it in the future.
That is my opinion and although some people might not agree I do share a good point, at least to me I do.

2 Likes

I agree with you, it should not matter if there is a better way to do it if your way works and there is no other tutorial regarding it, also very cool resource, even though I’m not interested in making a 2D game myself I do find it very nice.

Edit: Specially when it’s regarding scripting as there are multiple ways to achieve the same or a similar result.

5 Likes

I totally agree this could help someone, your code is simple and good to learn from, but it’s also bad practice meaning people should not learn to do things that way. The two things I suggested is fixing bad practice, not complicating anything. I was only trying to give constructive criticism.

4 Likes

I learned a lot of the stuff I know now about scripts from looking at other scripts. Now I can make simple and a few less simple scripts on my own. Sometimes when people reply like they did to my other tutorial which showed how to make a rounded part I get replies that don’t really have a nice tone. Most people said that things like Blender are way easier for the job but there are people out there that do not have access to such tools. A reply I got from that topic was something alone the lines of “Do you know how time consuming this is?” which was one of the more negative replies.

I hope this tutorial managed to at least help 1 person from having to spend months trying to figure out things like this. I was not joking when I said it took me months because I ran into many issues, bugs, and such. Even now my game isn’t bug free however it is functional.

I ended up learning a few useful features from just mentioning something. An example is instead of using wait() use task.wait().
I still have a long road ahead of me and at the current moment developing games is a hobby.

4 Likes

I say to TheOverseer, Great Job making a Tutorial! You took the time to share what you know, or learned and it is cool.

The more you do it, the more you will expand and grow. If you expand on it, feel free to share some more things!

They do no need to be a perfect tutorial, with the best code practices.

It is like public speaking, you got out in front of us, showed us some unique things, and should walk away with a smile on your face, and proud of what you did!

Thanks again, I am going to check it out more, and will share anything I create or expand on.

You Rock!

1 Like

Thanks for 25 likes on this topic everybody! I earned the “Good Topic” Badge too! I hope I managed to help at least 1 person out there, even if it was only a tiny bit. It took me months to make my game and hours to compile the bare minimum of the stuff needed to create something like my game and it’s inspiration into a tutorial.

It seemed like there was less arguments with this tutorial than my last one. This tutorial only had people that said I could use better scripts and stuff while on my last tutorial which was a simple one on how to make a rounded block people said that Blender is better (which it is) however I do not have access to such a tool.

If somebody ends up making a game using this tutorial let me know because I would love to see how it turns out!

1 Like