ID:1821330
 
I just wanted to make a little post about how I usually go about making things in various languages. I try and keep the same sort of convention for all of the programming languages I use, although there are some exceptions given that the syntax are different for each one. For this post I'll be strictly talking about DM.

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.
Another example using composition to create some stats for an actor:
///////////////
// Functions //
///////////////

proc/clamp(N,L,H) return max(min(N,H),L)

////////////
// Number //
////////////

number
var/min
var/max
var/value

proc/number_value(number/N) return N.value
proc/number_min(number/N) return N.min
proc/number_max(number/N) return N.max

proc/number_create(N=0,L=0,H=100)
var/number/n = new()

if (L > H)
n.min = H
n.max = L
else
n.min = L
n.max = H

n.value = clamp(N,L,H)
return n

proc/number_bounds_set(number/N,L,H)
if (L > H)
N.min = H
N.max = L
else
N.min = L
N.max = H

proc/number_set(number/N, V)
N.value = clamp(V,N.min, N.max)

proc/number_add(number/N, V)
N.value = clamp(N.value + V, N.min, N.max)

proc/number_mul(number/N, V)
N.value = clamp(N.value * V, N.min, N.max)

proc/number_to_stroing(number/N)
return "([N.min]<-[N.value]<-[N.max])"

///////////
// Actor //
///////////

actor
var/number/hp
var/number/mp
var/dead

proc/actor_hp(actor/A) return A.hp
proc/actor_mp(actor/A) return A.mp
proc/actor_dead(actor/A) return A.dead

proc/actor_create(H,M)
var/h = H*20
var/m = M*20
var/actor/a = new()
a.hp = number_create(h,0,h)
a.mp = number_create(m,0,m)
a.dead = FALSE
return a

proc/actor_damage(actor/A, D)
number_add(actor_hp(A), D)
if (number_value(actor_hp(A)) == 0)
actor_death(A)

proc/actor_death(actor/A)
A.dead = TRUE
Translated to non-Goober programming:
var app/app = new

app
var
const
MAX_CLIENTS = 20

running = TRUE
clients[MAX_CLIENTS]

New()
set waitfor = FALSE
while(running)
sleep world.tick_lag
Update()

proc
AddClient(Client)
. = clients.Find(null)
if(.) clients[.] = Client

RemoveClient(Client)
. = clients.Find(Client)
if(.) clients[.] = null

Update()
for(var/client/client in clients)
client.Update()

client
New()
return app.AddClient(src) && ..()

proc
Update()

entity
var
point
position = new
size = new (20, 20)

point
var x, y

New(X = 0, Y = 0)
Set(X, Y)

proc
Set(X, Y)
x = X
y = Y

Add(X, Y)
x += X
y += Y

Multiply(X, Y)
x *= X
y *= Y

ToString()
return "([x], [y])"

point3d
parent_type = /point

var z

New(X = 0, Y = 0, Z = 0)
..()
z = Z

Set(X, Y, Z)
..()
z = Z

Add(X, Y, Z)
..()
z += Z

Multiply(X, Y, Z)
..()
z *= Z

ToString()
return "([x], [y], [z])"

proc
clamp(n, l, h) return min(max(n, l), h)

bound_num
var
value
min
max

New(Value, Min = 0, Max = Value)
SetBounds(Min, Max)
SetValue(Value)

proc
SetValue(Value)
value = clamp(Value, min, max)

SetBounds(Min, Max)
if(Min <= Max)
min = Min
max = Max
else .(Max, Min)

Add(N)
SetValue(value + N)

Multiply(N)
SetValue(value * N)

ToString()
return "([min] < [value] < [max])"

actor
var
bound_num
hp
mp

dead = FALSE

New(HP, MP)
hp = new (HP)
mp = new (MP)

proc
TakeDamage(Damage)
if(!dead)
hp.Add(-Damage)
if(!hp.value)
Die()

Die()
dead = TRUE
In response to Kaiochao
Aaaaaoooooooh! ok! yep. YEP, i see it now... i really do, oh yes indeed, i now understand.
Was wondering about what exactly Mr Goober was up to over 'ere, but not to fear! Kaiochao is 'ere! Cheers for that bro. Phew. *thumbs up*++.


Edit: This actually isn't sarcasm btw... this time i'd say my jest just isn't one that carries over quite so well in text form :/... or actually i wonder if maybe this isn't as funny as i planned it, i also might be more affected by how long i've been awake than i thought...