This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
oeed's profile picture

[Restored] How to make a Graphical and Object Oriented OS

Started by oeed, 01 May 2013 - 02:18 AM
oeed #1
Posted 01 May 2013 - 04:18 AM
This is so bad it's not funny. Honestly. Half the stuff is wrong or done horribly. Save yourself, don't read it ;)/>

Thank you so much to QuantumGrav for saving and sending me a copy of the tutorial after the forums were rolled back!
I don't have much time at the moment so there may be some formatting problems.

Yesterday I was contacted by a fellow forums member on whether they could use PearOS’s code to make their own OS. They wanted to make an OS with an interface similar to Android, however, PearOS isn’t very suitable as it is heavily window based. It tried to find a tutorial in the Tutorials section that I could send back to him to get them started. To my surprise I couldn’t find a single one, there were tutorials on how to make buttons and so on, but none on how to make a fully blown graphical OS. After a quick Google I found 3-part video tutorial by NDF-Jay, but the coding practices weren’t very good and it didn’t really allow for much future development as nothing was modular. So, I’ve decided to make a tutorial on how to make an OS, that uses a very similar layout and system to PearOS, so you could say that this is also an explanation of how PearOS works. This OS will not have windows, it won’t be based on any existing OS. All it is designed to do is get you started and teach good and sustainable coding practices.

Prerequisites

What I highly recommend you use:
• An emulator, such as CCEmu or CCDesk. I’ll be using CCEmu because it’s less buggy, draw the screen a lot faster and it doesn’t trim the top pixels, which CCDesk does for some reason. See what I mean here. This may change in future though.
• A text editor. I highly recommend Sublime Text 2, it support OS X, Linux and Windows. But really, anything will do. Try and avoid using the built in editor, traversing a 1000+ line file in it is not nice.
• Patience. Don’t expect these things to come easy, I’ve been coding for almost 5 years and have had a lot of experience with Object-Oriented coding for a number of years. This tutorial will try to make the best and most efficient product it can, so it’s not going always going to use common and easy methods.

A Brief Intro to Object-Oriented Programming

Before we get stuck in creating our OS, I’d like to go over Object-Oriented Programming (referred to as OOP from now on). My explaination is going to be breif, and won’t be as good as other’s you may find on the internet, so I recommend you take a look around until you have a good idea of what it is.

For example, take a lemon. The lemon is in the citrus family, and it is a fruit. We could show this in Lua as the following:


Fruit = {
		Colour = colours.yellow,
}

Citrus = {
		__index = Fruit,
		PH = 3,
}

Lemon = {
		__index = Citrus,
}

In this (rather shocking) example, every Lemon has all the traits of Citrus, which has all the traits of Fruit. So the Lemon’s colour would be yellow and it would have a PH of 3. This is a very brief example, I highly recommend you check out this tutorial or others to get a better idea.

Program/Coding Style

Finally, before we begging I would like to go over the style and format of my code/programs.

First of all, I am from New Zealand (I’m just living in Australia, but the same applies), and rather than ‘color’ we use ‘colour’, so I’ll be using that through out the tutorial, however, you are welcome to use the spelling you’d like to use. I also tend to capitalise all function and variable names, just because it looks nicer. But it does not matter what you do in this regard, it still works exactly the same.

However, I also tend to break my programs up in to files (PearOS as around 70 files). But due to the nature of this tutorial, we won’t be doing that as much as I normally would. However, when I make a folder I request that you do that same so it’s easier to help you if you run in to trouble.

Anyway, on with the tutorial.

Getting Started

Open an Advanced Computer, if you are using an emulator (which you should) set the ID to 0, purely because its quicker if when you reopen the computer. Open the computers folder in your text editor and make a file called ‘startup’.

Enter the following in to the file:

if not term.isColour() then
		error("Please use an Advanced Computer")
end
shell.run("System/OS")
init()

What this code is doing is check that the computer supports colour, loading the OS then running the init method. We use ‘init’ to keep the code clean and make it obvious what code runs at startup, PearOS also uses an init method to catch errors, but we won’t be doing this.

Enter the following in to the file:


function init()
		term.setBackgroundColour(colours.lightGrey)
		term.clear()
   term.setCursorPos(22,10)
		term.write("Hello!")
end

If you’ve done everything correctly so far you should see “Hello!” in the middle of the screen. You may have noticed that we are taking quite a few steps to do 2 basic functions, and when we get in to drawing areas of colour this will only get worse. In every major project I’ve done in ComputerCraft (which is pretty much just PearOS and Heroes of Tlön, which never got released) I’ve used my (currently unreleased, but I might release it for the sake of it) Drawing API. If you have your own, take a look at how this works before you use your own, it’s essentiall that it has all the same features. You can find my Drawing API here (I haven’t looked at it for some time so it may have some faults), however, I encourage you to only take a look at it rather than use it for the tutorial.

A quick (or not so quick) note on naming objects.

In Objective-C (the main language for making OS X applications) most objects/group of functions start with prefixes, most of them have ‘NS’ (for example, ‘NSString’). In short NS stands for NExTSTEP (yes, that is the correct grammar/spelling/what ever you want to call it), OS X is based on it. If you want more information, Google it, it’s not important for our subject. Having a prefix is a great way knowing what an object is (e.g. a core operating system object, or the input from the user. You don’t want to accidentally write over an operating system object.). The prefix it used in PearOS was ‘OS’, I’m not going to use that purely so I don’t mix files up at some point, so I’ll be using CC (ComputerCraft) but it chose your own. It doesn’t matter, but keep it short and sweet.

Drawing Library

Now we are going to create our drawing library. As my prefix is ‘CC’ I’ll be calling it ‘CCDrawing’, I recommend you use the same format. At the top of the ‘OS’ file add the following code:



CCDrawing = {

		DrawCharacters = function (_x, _y, _string, _textColour, _backgroundColour)
				term.setBackgroundColour(_backgroundColour)
				term.setTextColour(_textColour)
				term.setCursorPos(_x, _y)
				term.write(_string)
		end,	

}

This will make drawing text in different colours a lot quicker and easier. It also allows you to easily implement a buffer, which we won’t do or discuss. In your init function replace the content with the below:


function init()
				CCDrawing.DrawCharacters(22,10, "Hello!", colours.white, colours.lightGrey)
		end

We have reduced our code from 4 lines to 1 line. (Yes, I know, technically we’ve increased it by around 10, but when you’re calling this dozens of times it does pay off.) If you run the code now you’ll see that while it does display the text, the reset of the screen is still black. Lets add a function to draw an area of colour, which we can use for many things from drawing a menu’s background to clearing the screen.

Add the following below the DrawCharacters end statement:



DrawArea = function (_x, _y, _w, _h, _character, _textColour, _bgColour)
				--width must be greater than 1, other wise we get a stack overflow
		   if _w > 0 then
						  _w = _w * -1
		   elseif _w == 0 then
						  _w = 1
		   end

		   if _h > 0 then
						  _h = _h * -1
		   elseif _h == 0 then
						  _h = 1
		   end

		   local sRow = ""
		   for ix = 1, _w do
				   sRow = sRow .. _character
		   end

		   for iy = 1, _h do
				   local currY = _y + iy - 1
				   CCDrawing.DrawCharacters(_x, currY, sRow, _textColour, _bgColour)
		   end
end

What this does is first makes sure that the width and height is greater than 0, it then makes a string that is a long as the width given and draws it so an area with the size given is made. Let's add this to our init code. Make sure you add this before the DrawCharacters line.

CCDrawing.DrawArea(1,1,51,19," ",colours.white, colours.lightGrey)

You may have noticed that I've used 51 and 19 for the width and height, these are the screen dimentions. However, you should never use set numbers. Let's add a value to the drawing api so we can easily get the screen size with out having to dedicate a few lines to retrieve them.

At the very top of the code add this line:

local _w, _h = term.getSize()

This gets our screen size and stores it in two variables. We then assign those values to the screen size, add this at the top of the drawing code:


Screen = {
  Width = _w,
  Height = _h
},

Now we can change our DrawArea line to this:

CCDrawing.DrawArea(1, 1, CCDrawing.Screen.Width, CCDrawing.Screen.Height, " ", colours.white, colours.lightGrey)

Now, there are other ways to do this, this is one of the tidier and easier ways. It doesn't change when the screen size changes, but if you want to add that it's not too hard.

Making Buttons

Alright, lets make our first button!

Our button will be be a subclass (meaning a child/descendant, like the Lemon or Citrus was) of three objects in this order:
CCButton

CCControl

CCEntity

CCObject
Each part of that tree has its own functions. CCButton deals with all of the drawing code and creation of a new button. CCControl handles the clicking, whether or not it's disabled, whether it's selected, etc. CCEntity handles the position and size. Finally, CCObject manages the objects ID.

Now, you may be asking your self why we bother having all these different objects. The answer is simple. All the functions that CCButton has to do are specific to it self, nothing else can use them. However, if we want to add more controls later on such as a menu then we can simply make it a subclass of CCControl. We can do the same for CCEntity, which is essentially anything thats on the screen (in the case of PearOS, the dock for example). And every single object inherits from CCObject so we can keep track of them and add functions that all objects need. This is exactly how PearOS works and very very close to how OS X works.

Lets start with CCObject. Immediately after CCDrawing finishes add the following code:


CCObject = {
		Type = "CCObject",
		ID = 0
}


Not much happens in OSObjects, buts its good to have.

Now we'll add CCEntity, after CCObject finishes add the following:


CCEntity = {
		__index = CCObject,

		X = 0,
		Y = 0,
		Width = 0,
		Height =  0,
		Title = "",
}

A few things happening here. '__index' is the object is inherits from (as Lemon does from Citrus). X, Y, etc. are the possition and size of the object. Title is the title of the object which most entities will have.

Let now add CCControl, again, add this below CCEntity.


CCControl = {
		__index = CCEntity,
		Action = nil,
		Enabled = true
}

Not a huge amount going on here, but it is important. Action is the function that is called when the object is clicked and enabled is whether or not the object can be clicked.

Finally, lets add CCButton.


CCButton = {
		__index = CCControl,
		Type = "CCButton",
		TextColour = colours.white,
		BackgroundColour = colours.grey,
		Height = 1,
		New = function(self, _x, _y, _title, _action)
		  local new = {}		-- the new instance
		  setmetatable( new, {__index = self} )
		  new.Width = string.len(_title)+2
		  new.X = _x
		  new.Y = _y
		  new.Title = _title
		  new.Action = _action
		  new.Enabled = true
		  return new
		end,
		Draw = function(self)
				CCDrawing.DrawCharacters(self.X,self.Y, " "..self.Title.." ", self.TextColour, self.BackgroundColour)
		end
}

Self referes to the object that called the function, however, to do this you have to use a colon ( : ), so for example 'ourButton:Draw()' is the same as ourButton.Draw(ourButton). It's just a way to shorten your code and make sure you are getting the right object.
TextColour and BackgroundColour are the, as their name suggests, the text and background colours. We do this so we can change the colours easily. Height is 1 by default, you can change this, or you could add it as another argument in New; but for now 1 is fine. The New function is called when ever we need a new instance of a button and Draw is called when we want to draw it on the screen.

Let's test our new button framework, remove the 'DrawCharacters' line in init and replace it with this:

local ourButton = CCButton:New(19, 10, "Hello Again!", function() os.reboot() end)
ourButton:Draw()

If you've done everything correctly you should see a grey box in the middle of the screen with "Hello Again!". If you don't, try to find the error and see if you can fix it on your own. If you've tried to fix it but you can't post the error message below.

You may have noticed at the end of the the first line that we have a function with os.reboot in it. This will be called when we click it. However, we don't have anyway of detecting whether the button has been clicked, so lets do that now.

At the very top of the file, just after the term.getSize line, add the following:


CCInterfaceEntities = {

		List = {},

   Add = function(_entity)
		  table.insert(CCInterfaceEntities.List, _entity)
   end

}
This is our place to store our on-screen objects (entities), every time the screen is clicked or drawn it will go though the list and either find the object to click or draw them. We have an Add method so we can easily add objects, you could add a remove method if you want but we won't be doing this.

Now that we have our interface entities list, remove the 'ourButton:Draw()' line and replace it with this:

CCInterfaceEntities.Add(ourButton)

If you run your code now, you shouldn't see your button, this is because it's no being draw yet. Lets add a drawing function. Place this just before init.


function DrawScreen()
for _,entity in ipairs(CCInterfaceEntities.List) do
  entity:Draw()
end
end

This goes through every entity on the list and draws it.

Capturing Mouse Clicks

Now we need to add away to capture clicks. Add this just after the DrawScreen function.


function EventHandler()
while true do
  local event, arg, x, y = os.pullEventRaw()
  if event == "mouse_click" then
   for _,entity in pairs(CCInterfaceEntities.List) do
		--check if the click overlaps an entities
		if x >= entity.X and x >=entity.X + entity.Width - 1 and y >= entity.Y and y >=entity.Y + entity.Height then
		 if entity.Action then
		  entity:Action()
		  break
		 end
		end
   end
  end
end
end

This waits for an event, and if it's a mouse click it will, as the drawing function does, loop through all of the entities and see if the click overlaps the entity, if it does it checks if the entity has an action and if it does it runs it. After the action has been run the loop breaks (stops) to prevent any other entities being clicked.

So, thats the end of the tutorial. I hope you've learnt a lot and taken some valuable practices etc. out of it. You can use this code as you wish, make your own OS out of if you like. You don't have to credit me, but it would be appreciated if you link back to this tutorial. Remember, this is very brief and can't really be called an OS yet, even by ComputerCraft standards. I have left it to you to make the OS of your dreams with a solid framework behind it. If you have any problems or questions leave a comment below or PM me. You can find this tutorials code (the OS file, there's no point uploading the two line startup file) here. However, I encourage you to only use it you have a problem with your code.

Download the Tutorial code.
Edited on 13 July 2015 - 05:03 AM
Dlcruz129 #2
Posted 01 May 2013 - 09:58 AM
Cookehs to QuantumGrav for saving this tutorial, and to you for writing it!

EDIT: Your download button is just plain text now.
Sammich Lord #3
Posted 01 May 2013 - 11:13 AM
You need to set the metatables man. Setting the __index won't do anything without a metatable.
remiX #4
Posted 01 May 2013 - 12:10 PM
Tut code is broken
diegodan1893 #5
Posted 01 May 2013 - 01:16 PM
If you want the tutorial code I found it in Google: http://pastebin.com/QrsAawAz


You need to set the metatables man. Setting the __index won't do anything without a metatable.

He did that here:


New = function(self, _x, _y, _title, _action)
		  local new = {}		-- the new instance
		  setmetatable( new, {__index = self} ) -- <------------- Here
		  new.Width = string.len(_title)+2
		  new.X = _x
		  new.Y = _y
		  new.Title = _title
		  new.Action = _action
		  new.Enabled = true
		  return new
		end,
Sammich Lord #6
Posted 01 May 2013 - 01:25 PM
If you want the tutorial code I found it in Google: http://pastebin.com/QrsAawAz


You need to set the metatables man. Setting the __index won't do anything without a metatable.

He did that here:


New = function(self, _x, _y, _title, _action)
		  local new = {}		-- the new instance
		  setmetatable( new, {__index = self} ) -- <------------- Here
		  new.Width = string.len(_title)+2
		  new.X = _x
		  new.Y = _y
		  new.Title = _title
		  new.Action = _action
		  new.Enabled = true
		  return new
		end,
Since he said it was restored and didn't do it last time I assumed it wasn't there. I should've looked first.

Edit: After looking over the post again there are still tables that have __index but don't have their metatable set.
Jarle212 #7
Posted 01 May 2013 - 01:44 PM
If you want the tutorial code I found it in Google: http://pastebin.com/QrsAawAz


You need to set the metatables man. Setting the __index won't do anything without a metatable.

He did that here:


New = function(self, _x, _y, _title, _action)
		  local new = {}		-- the new instance
		  setmetatable( new, {__index = self} ) -- <------------- Here
		  new.Width = string.len(_title)+2
		  new.X = _x
		  new.Y = _y
		  new.Title = _title
		  new.Action = _action
		  new.Enabled = true
		  return new
		end,
Since he said it was restored and didn't do it last time I assumed it wasn't there. I should've looked first.

Edit: After looking over the post again there are still tables that have __index but don't have their metatable set.

I think those are ment to be static and not for making instances out of.
Sammich Lord #8
Posted 01 May 2013 - 02:26 PM
I think those are ment to be static and not for making instances out of.
Why would he have a __index on them if they are meant to be static?
diegodan1893 #9
Posted 01 May 2013 - 03:26 PM
Why would he have a __index on them if they are meant to be static?

Because these tables are going to be the parent "class" for more objects. For example, CCButton extends from CCControl. If CCControl doesn't have CCEntity as __index, CCButton won't be able to acess CCEntity variables.
Sammich Lord #10
Posted 01 May 2013 - 03:32 PM
Why would he have a __index on them if they are meant to be static?

Because these tables are going to be the parent "class" for more objects. For example, CCButton extends from CCControl. If CCControl doesn't have CCEntity as __index, CCButton won't be able to acess CCEntity variables.
Tables can store anything. __index is just a normal value if you don't set a metatable. So you would never set __index if you didn't want to set a metatable.
Jarle212 #11
Posted 01 May 2013 - 04:12 PM
I think those are ment to be static and not for making instances out of.
Why would he have a __index on them if they are meant to be static?

Hmm not shure. The example used in the tutorial whould not work, but if you do like this:


CCObject = {
   type = "CCObject",
   main = "CCObject"
}
CCEntity = {
   __index = CCObject,
   type = "CCEntity",
   width = 200
}
CCButton = {
   __index = CCEntity,
   type= "CCButton",
   new = function(self,x,y)
          newB = {}
          newB.x = x
          newB.y = y
          setmetatable(newB,{__index = self})
 return newB
   end
}
setmetatable(CCEntity,CCEntity)
setmetatable(CCButton, CCButton)
testButton = CCButton:new(5,5)
print(testButton.main) --Would give CCObject.main
print(testButton.width) --Would give CCEntity.width
print(testButton.type) --Would give CCButton.type
print(testButton.x) --Would give testButton.x

Edit: so many edits :P/>

Edit[insert_number]: finaly got it right
Edited on 01 May 2013 - 02:30 PM
rhysjack7 #12
Posted 12 May 2013 - 07:05 AM
Please Can You PM Me If
System/OS
Is An Api
DarkEspeon #13
Posted 14 August 2013 - 10:49 PM
in the tutorial at CCDrawing.DrawArea, _w > 0 and _h > 0 should be _w < 0 and _h < 0 in their respective spots, otherwise DrawArea will never work due to _w and _h being negative
gjgfuj #14
Posted 07 September 2013 - 01:10 AM
Your code makes me shudder. Brr…

Those coding conventions are terrible! They can be used in objective C. But not lua.

For lua, I would reccomend doing this sort of thing.

myThing = {}
myThing.blah = function ()
print("Hello!")
end

Do you understand? Use a table as a namespace. Then there is no need for prefixes!

Or, using your example,

CC = {}
CC.Object = {
   type = "Object",
   main = "Object"
}
CC.Entity = {
   __index = Object,
   type = "Entity",
   width = 200
}
CC.Button = {
   __index = Entity,
   type= "Button",
   new = function(self,x,y)
		  newB = {}
		  newB.x = x
		  newB.y = y
		  setmetatable(newB,{__index = self})
return newB
   end
}
oeed #15
Posted 07 September 2013 - 03:06 AM
Your code makes me shudder. Brr…

Those coding conventions are terrible! They can be used in objective C. But not lua.

For lua, I would reccomend doing this sort of thing.

myThing = {}
myThing.blah = function ()
print("Hello!")
end

Do you understand? Use a table as a namespace. Then there is no need for prefixes!

Or, using your example,

CC = {}
CC.Object = {
   type = "Object",
   main = "Object"
}
CC.Entity = {
   __index = Object,
   type = "Entity",
   width = 200
}
CC.Button = {
   __index = Entity,
   type= "Button",
   new = function(self,x,y)
		  newB = {}
		  newB.x = x
		  newB.y = y
		  setmetatable(newB,{__index = self})
return newB
   end
}

Some of this code is old and a little buggy. However, I'm not really sure you should be going around to peoples posts calling their conventions crap, especially conventions of one of the most loved OSs on the forums…
Lyqyd #16
Posted 07 September 2013 - 12:24 PM
He has a good point, though. Using a table to contain the grouped tables like that puts only one thing in the global namespace rather than several. It is generally a good practice to minimize the number of things one puts directly into the global namespace.
Bubba #17
Posted 07 September 2013 - 12:31 PM
However, I'm not really sure you should be going around to peoples posts calling their conventions crap, especially conventions of one of the most loved OSs on the forums…

By that logic, Galileo should not have questioned the Geocentric model simply because many people believed it to be true.