At the end of last year I started to look into how to make my code simpler by using simple design methods. This omits certain aspects of object-oriented programming such as inheritance and polymorphism in favor of functions and structs. It's more or less the same, except that this approach is much more procedural.
When I program, I make sure to create modules which handle different aspects of the project. However, there is always one global object which holds everything about said project. This can be a global object name like app, game, etc..
Defining the App object in DM is always easy, since it's a datum.
app
What does an app contain? Well, let's see. We know that clients connect to the app remotely if it's being hosted, and if running in singleplayer mode, the client is still a client. So, we can make a client array (not a dynamically sized list, for reasons I'll explain later).
///////////////
// Constants //
///////////////
var/const
APP_CLIENTS_MAX = 20
/////////////
// Globals //
/////////////
var/app/app
//////////
// Type //
//////////
app
var/running
var/list/clients
///////////////
// Functions //
///////////////
proc/app_create()
var/app/a = new()
a.clients = new/list(APP_CLIENTS_MAX)
a.running = TRUE
return a
proc/app_clients_add(client/C)
// finds an empty slot in the array and returns the slot while
// also adding the client to that slot.
for (var/i = 1 to app.clients.len)
if (app.clients[i] == null)
app.clients[i] = C
return i
return 0
proc/app_clients_remove(client/C)
// finds the client reference in the array and removes it.
for (var/i = 1 to app.clients.len)
if (app.clients[i] == C)
app.clients[i] = null
return i
return 0
proc/app_running()
return app.running
proc/app_update()
for (var/i = 1 to app.clients.len)
if (app.clients[i] != null)
client_update(app.clients[i])
///////////
// World //
///////////
world
New()
..()
app = app_create()
spawn()
while (app_running())
app_update()
////////////
// Client //
////////////
client
New()
..()
app_clients_add(src)
Del()
app_clients_remove(src)
..()
proc/client_update(client/C)
So what I have above is a bare-bones application in which clients are added to app.clients and removed from it when they disconnect. The purpose of keeping this list a strict array is so that clients will always have a specific ID which do not change, and can be referenced by this ID as an index in the array.
Clients also update once per tick using client_update(). Why are functions not attached to objects? Well, I partially dislike that functions do not become accessible when an object is not present. I prefer to keep functions on a global level so that they can be accessed anywhere. There's also the readability aspect of the function where objects have their type name prefixed in the function before the command itself (client_update() and app_clients_add(), for example). Comments aren't really necessary because the command name tells you exactly what it does and where it's supposed to be used.
Let's shift to a different example. Let's say we want to make an entity with a position and a size:
////////////
// Entity //
////////////
entity
var/point/position
var/point/size
proc/entity_create()
var/entity/e = new()
e.position = point_create(0,0)
e.size = point_create(20,20)
return e
proc/entity_position(entity/E) return E.position
proc/entity_size(entity/E) return E.size
since a point object doesn't exist, I'll make that too:
///////////
// Point //
///////////
point
var/x
var/y
proc/point_create(X=0,Y=0)
var/point/p = new()
p.x = X
p.y = Y
return p
proc/point_x(point/P) return P.x
proc/point_y(point/P) return P.y
proc/point_set(point/P,X,Y)
P.x = X
P.y = Y
proc/point_add(point/P,X,Y)
P.x += X
P.y += Y
proc/point_mul(point/P,X,Y)
P.x *= X
P.y *= Y
proc/point_to_string(point/P)
return "([P.x],[P.y])"
An entity really only contains two point objects which in turn contain the coordinates of the position and size for it. So, if we wanted to grab the value of the entity e's x position, we would say "e.position.x" or "point_x(entity_position(e))". Either way works just fine. An entity's abiltiy to be extended depends entirely on what it contains. This means that an entity can have multiple objects which characterize its behavior. This method of design is called the Composite Reuse Principle or Composition for short. In the example above, the /point datum serves as a reusable object anywhere we may need it. This is not unlike /list datums.
Since inheritance isn't apparent in this method of design, extending child types isn't really a thing. Instead if we wanted to make an extension, we would do so my defining that datum and containing the parent type inside of it. Let's make a 3D point which contains a 2D point plus another variable z.
//////////////
// Point 3D //
//////////////
point3d
var/point/point
var/z
proc/point3d_create(X=0,Y=0,Z=0)
var/point3d/p = new()
p.point = point_create(X,Y)
p.z = Z
return p
proc/point3d_set(point3d/P,X,Y,Z)
point_set(P.point, X, Y)
P.z = Z
proc/point3d_add(point3d/P,X,Y,Z)
point_add(P.point, X, Y)
P.z += Z
proc/point3d_mul(point3d/P,X,Y,Z)
point_mul(P.point, X, Y)
P.z *= Z
proc/point3d_to_string(point3d/P)
return "([P.point.x],[P.point.y],[P.z])"
That's pretty much all there is to that. I would normally make a 3D point without using the /point object, but this explains how things can be extended without inheritance. The user needs only to know the command set in order to use it.
Welp, that's all I have for now. See ya.