So things have gotten quite interesting lately. where to begin...
I may no longer be writing code in DBpro.
( No wait, read on! really

)
As mentioned previously, I've begun to work on an editor for the game. This is built on the game itself as a copy of the project with added developmental features and editing tools.
Keeping the two projects in sync with each other, while at the same time restricting what funcitonality the production version is allowed to include becomes quite an effort (and error prone) in itself. Add in that I will eventually want to seperate out all the debugging and logging code from the production version into debug and release builds and I'm essentially looking at managing 3 project versions. not good.
Now, I've recently been using
Gulp with
Typescript and
React quite a lot for web-developent, and using my experience with these, I began to think about how to bring a build system into DBpro.
Initially, the intent was merely to manage environment scoping so that I only had to maintain a single version of the project source and could indicate to the build system via scope tags what to include into which build (editor, debug, release).
Something like this:
print "I will appear in all builds"
<!
print "I will only appear in the editor build
rem This comment will only appear in the editor build
!>
<? print "I will appear in both editor and debug builds" ?>
rem I am a comment outside of environment scope, I will technically appear in 'all' builds, but will be removed from release for optimization as comments are not needed in the final build
print "I still get included in all of them though!"
In which anything wrapped into <! !> tags would be included only the in the editor build files. <? ?> would be included in editor and debug builds, and anything outside of scope tags will be included in all builds.
This would require writting a relatively simple preprocesor that would take a source file as input and create several new source files as output, one for each environment.
Then I thought, well shoot. If I am running a preprocessor over everything anyways, why not expand it some. We can standardize the syntax of DBpro commands:
Instead of space cased commands, in which only commands that return values use parenthesis () we can make all of them camelCased() and enclosed. We can fix the ambiguity between arrays and functions by making arrays use []. These can be handled with simple regex replacements.
array insert at top SomeArray()
myCount = array count(SomeArray())
Becomes:
@arrayInsertAtTop(SomeArray[])
myCount = @arrayCount(SomeArray[])
(and of course you can still just write raw dbpro 'array insert at top SomeArray()' and it will get transpiled over 1:1)
So now the excitement is building and possibilities are spinning through my mind. OK, why not add some common array methods as shorthand?
SomeArray[].$push("something")
can easily transpile into:
array insert at bottom SomeArray()
SomeArray() = "something"
But wait! Why not implement some OOP!
If you will from a previous post in this thread, my framework and coding practices in DBpro already make use of some some OOP fundamentals *by convention*, if we are now going to programmatically re-write and generate dbpro code anyways, let's just go all the way!
So, I've started creating a sort of 'Dark Basic Enhanced' language which uses a .dbx file.
I've created a transpiler which will convert the .dbx source to .dba source (you still need to compile the final .dba source with the dbpro compiler as normal to get an .exe, but the build system handles this as well)
I've created a build system which will manage the scoping of environments, abstracts the use of the transpiler, and will eventually handle optimization of media (.x to .dbo, .png to .dds etc)
Lastly, I've created a project setup tool which will setup new projects based on templates and which is designed so that users can easily define their own project templates.
Of course the DBpro IDE's aren't compatible with .dbx, but then I mostly only use the IDE to compile anyways with the occasional syntax lookup on a keyword. For a long time now all of my coding has been done in notepad++, but I've recently begun to use Sublime Text 3 and I *highly* recommend it. I can now execute a compile via the build system and so I no longer use the DBpro IDE at all.
This system makes use of several different applications written in C# and DBpro. I'm tentatively calling it 'Obsidian' (from OBS: [O]bject oriented [B]uild [S]ystem) I like the sound and it gives a nod to the 'dark' naming theme of DarkBasic related things.
It is still fairly fragile and in a very early stage of developement, but so far , it has transpiled this dbx:
// Prefixing a function with {{atsymbol}} will expand it into a DBpro syntax command during the build process
// Special functions and directives are prefixed with $ and can often provide a shorthand replacement for several basic commands
//-------------------------------------
$Class Character(name as string, age as integer) {
type statData {
strength as integer
agility as integer
endurance as integer
health as integer
mana as integer
}
//private data ----------------------
private age as integer
private stats as statData
//public data -----------------------
public name as string
//private methods -------------------
private function init() {
this.name = name
this.age = age
this.stats.strength = 8
}
//public methods --------------------
public function speak() {
@print("My name is: " + this.name + " and I am " + str$(this.age))
}
public function setStrength(reqVal as integer) {
this.stats.strength = reqVal
@print("my strength is now: " + str$(reqVal))
}
public function destroy() {
}
}
@print("Instantiate an object and call a method:")
myChar = new Character("Ortu", 99)
myChar.speak()
@print(" ")
@print("Press a key to continue...")
@print(" ")
@waitKey()
@print("name is a public member, let's change it:")
myChar.name = "Bob"
myChar.speak()
@print(" ")
@print("you can also access the public member directly:")
@print(myChar.name)
@print(" ")
@print("Press a key to continue...")
@print(" ")
@waitKey()
//
//age is private and cannot be accessed directly by an object.
//These will throw a transpiler error if not commented:
//myChar.age = 50
//@print(myChar.age)
//myChar.stats.strength is also private, but you can access it using the public method myChar.setStrength(someValue)
@print("set some private data using a public method:")
myChar.setStrength(4)
@print(" ")
@print("Press a key to continue...")
@print(" ")
@waitKey()
@print("make a second char:")
anotherChar = new Character("Tim", 42)
anotherChar.speak()
@print(" ")
@print("Press a key to continue...")
@print(" ")
@waitKey()
@print("lets do some grouping. We will add multiple characters to an array, then use")
@print(" .speak() for each item in the array:")
dim characters[] as $Character
@arrayInsertAtBottom(characters[])
characters[] = myChar
characters[].$push(anotherChar)
characters[].$foreach(){
//keyword 'this' will refer to current item under foreach iteration
this.speak()
}
@print(" ")
@print("Press a key to continue...")
@print(" ")
@waitKey()
@print("Referencing an invalid object throws an exception. (This will crash)")
@print(" ")
@print("Press a key to continue...")
@waitKey()
myChar.destroy()
myChar.speak()
@print(" ")
@print("Press a key to continue...")
@print(" ")
@waitKey()
@end()
into this programatically generated dba:
`TEMP: obsidian.dba code will go into a dedicated module within the framework. Include here for isolated testing.
`obsidian.dba ---------------------
type Obsidian_ClassObject
instanceID as string
initArgs as string
endtype
dim Obsidian_Scope_This() as string
`main.dba ---------------------
REMSTART
This source code was generated by the Obsidian Transpiler
REMEND
gosub init
gosub main
init:
`class definitions =========================
`Class: Character
`declare types with class namespace
type Character_Type_statData
instanceID as string
strength as integer
agility as integer
endurance as integer
health as integer
mana as integer
endtype
type Character_PrivateData
instanceID as string
age as integer
stats as Character_Type_statData
endtype
type Character_PublicData
instanceID as string
name as string
endtype
`setup class object and data containers
dim Character_Object() as Obsidian_ClassObject
dim Character_Member() as Character_PublicData
dim _Character_Member() as Character_PrivateData
return
`setup class data reference getters
`Class: Character
function Character_referenceObject(instanceID as string, cacheIndex as integer)
o = -1 : i = -1 : _i = -1
`try cache
if cacheIndex > -1 and cacheIndex <= array count(Character_Object())
if Character_Object(cacheIndex).instanceID = instanceID
exitfunction cacheIndex
endif
endif
`get new reference
for peek = 0 to array count(Character_Object())
if Character_Object(peek).instanceID = instanceID
o = peek
exit
endif
next peek
`handle not found
assert error o > -1, "ERROR: Failed to get reference to requested object. This object may no longer be valid.", "Obsidian Error"
endfunction o
function Character_referencePublicMembers(instanceID as string, cacheIndex as integer)
o = -1 : i = -1 : _i = -1
`try cache
if cacheIndex > -1 and cacheIndex <= array count(Character_Member())
if Character_Member(cacheIndex).instanceID = instanceID
exitfunction cacheIndex
endif
endif
`get new reference
for peek = 0 to array count(Character_Member())
if Character_Member(peek).instanceID = instanceID
i = peek
exit
endif
next peek
`handle not found:
assert error i > -1, "ERROR: Failed to get reference to requested object. This object may no longer be valid.", "Obsidian Error"
endfunction i
function Character_referencePrivateMembers(instanceID as string, cacheIndex as integer)
o = -1 : i = -1 : _i = -1
`validate scope
array index to top Obsidian_Scope_This()
if array index valid(Obsidian_Scope_This())
if instanceID <> Obsidian_Scope_This()
`report error "Warning: Failed to access private member from external scope. Class definition: file: C:\dev\Obs Test\src\main.dbx, line: 8", "Obsidian Error"
exitfunction _i
endif
else
`report error "Warning: Failed to access private member from external scope. Class definition: file: C:\dev\Obs Test\src\main.dbx, line: 8", "Obsidian Error"
exitfunction _i
endif
`try cache
if cacheIndex > -1 and array index valid(_Character_Member(cacheIndex))
if _Character_Member(cacheIndex).instanceID = instanceID
exitfunction cacheIndex
endif
endif
`get new reference
for peek = 0 to array count(_Character_Member())
if _Character_Member(peek).instanceID = Obsidian_Scope_This()
_i = peek
exit
endif
next peek
`handle not found
assert error _i > -1, "ERROR: Failed to get reference to requested object. This object may no longer be valid.", "Obsidian Error"
endfunction _i
`setup class method functions
`Class: Character
function Character_construct_Character(name as string, age as integer)
o = -1 : i = -1 : _i = -1
instanceID as string
instanceID = generate guid$()
`push this to reference stack
array insert at top Obsidian_Scope_This()
Obsidian_Scope_This() = instanceID
`instantiate an object
array insert at bottom Character_Object()
Character_Object().instanceID = instanceID
Character_Object().initArgs = name + "," + str$(age)
`instantiate object data
array insert at bottom Character_Member()
Character_Member().instanceID = instanceID
array insert at bottom _Character_Member()
_Character_Member().instanceID = instanceID
_Character_init(instanceID)
`pop this from reference stack
array delete element Obsidian_Scope_This(), 0
endfunction instanceID
function Character_destruct_Character(instanceID as string)
o = -1 : i = -1 : _i = -1
`push this to reference stack
array insert at top Obsidian_Scope_This()
Obsidian_Scope_This() = instanceID
o = Character_referenceObject(instanceID, o)
if o > -1
array delete element Character_Object(), o
endif
i = Character_referencePublicMembers(instanceID, i)
if i > -1
array delete element Character_Member(), i
endif
_i = Character_referencePrivateMembers(instanceID, _i)
if _i > -1
array delete element _Character_Member(), _i
endif
`pop this from reference stack
array delete element Obsidian_Scope_This(), 0
endfunction
function _Character_init(instanceID as string)
o = -1 : i = -1 : _i = -1
`push this to reference stack
array insert at top Obsidian_Scope_This()
Obsidian_Scope_This() = instanceID
o = Character_referenceObject(instanceID, o)
if o > -1
split string Character_Object(o).initArgs, ","
i = Character_referencePublicMembers(instanceID, i)
if i > -1
Character_Member(i).name = get split word$(1)
endif
_i = Character_referencePrivateMembers(instanceID, _i)
if _i > -1
_Character_Member(_i).age = intval(get split word$(2))
endif
_i = Character_referencePrivateMembers(instanceID, _i)
if _i > -1
_Character_Member(_i).stats.strength = 8
endif
endif
`pop this from reference stack
array delete element Obsidian_Scope_This(), 0
endfunction
function Character_speak(instanceID as string)
o = -1 : i = -1 : _i = -1
`push this to reference stack
array insert at top Obsidian_Scope_This()
Obsidian_Scope_This() = instanceID
i = Character_referencePublicMembers(instanceID, i)
_i = Character_referencePrivateMembers(instanceID, _i)
if i > -1 and _i > -1
print "My name is: " + Character_Member(i).name + " and I am " + str$(_Character_Member(_i).age)
endif
`pop this from reference stack
array delete element Obsidian_Scope_This(), 0
endfunction
function Character_setStrength(instanceID as string, reqVal as integer)
o = -1 : i = -1 : _i = -1
`push this to reference stack
array insert at top Obsidian_Scope_This()
Obsidian_Scope_This() = instanceID
_i = Character_referencePrivateMembers(instanceID, _i)
if _i > -1
_Character_Member(_i).stats.strength = reqVal
endif
print "my strength is now: " + str$(reqVal)
`pop this from reference stack
array delete element Obsidian_Scope_This(), 0
endfunction
function Character_destroy(instanceID as string)
o = -1 : i = -1 : _i = -1
`push this to reference stack
array insert at top Obsidian_Scope_This()
Obsidian_Scope_This() = instanceID
Character_destruct_Character(instanceID)
`pop this from reference stack
array delete element Obsidian_Scope_This(), 0
endfunction
main:
print "Instantiate an object and call a method:"
myChar as string
myChar = Character_construct_Character("Ortu", 99)
Character_speak(myChar)
print " "
print "Press a key to continue..."
print " "
wait key
print "name is a public member, let's change it:"
i = Character_referencePublicMembers(myChar, i)
if i > -1
Character_Member(i).name = "Bob"
endif
Character_speak(myChar)
print " "
print "you can also access the public member directly:"
i = Character_referencePublicMembers(myChar, i)
if i > -1
print Character_Member(i).name
endif
print " "
print "Press a key to continue..."
print " "
wait key
print "set some private data using a public method:"
Character_setStrength(myChar, 4)
print " "
print "Press a key to continue..."
print " "
wait key
print "make a second char:"
anotherChar as string
anotherChar = Character_construct_Character("Tim", 42)
Character_speak(anotherChar)
print " "
print "Press a key to continue..."
print " "
wait key
print "lets do some grouping. We will add multiple characters to an array, then use"
print " .speak() for each item in the array:"
dim characters() as string
array insert at bottom characters()
characters() = myChar
array insert at bottom characters()
characters() = anotherChar
for fe = 0 to array count(characters())
Character_speak(characters(fe))
next fe
print " "
print "Press a key to continue..."
print " "
wait key
print "Referencing an invalid object throws an exception. (This will crash)"
print " "
print "Press a key to continue..."
wait key
Character_destroy(myChar)
Character_speak(myChar)
print " "
print "Press a key to continue..."
print " "
wait key
end
return
which compiles and runs as expected
You can paste the dba code above into an empty project and run it to have a look (requires Matrix1)
There may be some unavoidable tradeoff between having OOP oraganization and structure versus the performance cost of implementing that in a proceedural language. It obviously has to do more lookups to access data references, so I will be interested to see how Sulium's performance compares when using this system to its previous.
As to compile times, well you still compile regular .dba with the regular dbpro compiler, no change there. But you do add the added step of preprocessing and transpiling. (The above file transpiles in ~50ms) As this is done in C#, it does make use of multithreading to process multiple files simultaneously, and I have plans to implement checks so that it only processes files that have actually changed for added optimization. But as talked about previously one of the best things you can do for compile times is to abstract and load as much in from external sources as possible. reduce hard coded content so that you can change values without having to recompile. This should be done anyway.
Well, I think I will split further discussion of Obsidian itself to its own thread, and address it here only as it relates to developing Sulium using it. The abstract modular framework that Sulium is built on will be wrapped into Obsidian as part of the default project template.