Writing a Roguelike in D, C# and C++



Greetings Everyone,

For the last 7DRL event, I tried two new things at once: learning a new
programming language and writing my first roguelike. My first attempt
was with D, but that crashed and burned. My second attempt was with C#.
That slipped the surly bonds of earth, but it poked along more like a
747 than the SR-71 I had hoped. So for my third attempt, I went back to
my trusty old favorite C++.

Since I have seen some interest on this newsgroup about which language
to use, I am detailing my impressions of the different languages
below. I have tried to limit my observations to things that I noticed
while developing the game. Although I know C++ well, this exercise
made me see more clearly some difficulties in C++ that I had forgotten
about long ago.

Some of the issues I raise are problems with the quality of
implementation. I can only use what is available. So for the record
I am developing on 64 bit Linux (Debian testing). For D, I used the
D2 version of gdc. For C#, I used the Debian package of mono 2.4.4.
For C++, I used g++ 4.4.3 with C++0X extensions (loooove those
extensions). All three languages have decent interfaces with libtcod.

For your amusement, I have made it a bit of a rant. Some things are
trivial, some are not.

It is quite possible that I committed a grievous performance sin
in the C# code, so feel free to debug it for me ;). Using bzr

http://bazaar.canonical.com/en/

you can get the C++ and C# versions with the commands

bzr clone http://www.gps.caltech.edu/~walter/Pit_of_Hate/cxx
bzr clone http://www.gps.caltech.edu/~walter/Pit_of_Hate/csharp

and follow the instructions in README. It is not exciting as a game.
You can only run around the screen while monsters chase you. No combat
etc.

For those keeping score, sloccount says the C# code totals 979 lines
after I comment out some unused code. The equivalent C++ code totals
1150.

Cheers,
Walter Landry



D
------

The Good:
---------

1) Fast compilation

My entire project (which is admittedly not large) compiled in under
a second. This makes it easy to quickly try out incremental
changes. It felt like I was developing with the rapid-turnaround
of a dynamic language (e.g. Python or Ruby) but with the
type-safety and speed of a compiled language. Woot! This was way
cool.

2) Modules

No circular dependency issues. No preprocessor gotcha's. No
include guards. Not as clumsy or random as header files; an elegant
method for a more civilized language.

The Bad:
--------

3) Tools really, really suck

D is transforming itself from version 1 to version 2. So you have
to choose which language to learn. I chose D2, since it seemed
like a nicer language and where all the energy is. I seem not to
be the only one thinking this way.

http://www.micropoll.com/akira/mpresult/928402-256449

Unfortunately, D2 is not quite done.

The official compiler is dmd. The front end is open source, but
the back end is not. That means that, among other things, there is
no support for 64 bit Linux or Windows. This is not encouraging
for a language that has been around for over a decade.

So for 64 bit support you are forced to use either the port to gcc
(gdc) or llvm (ldc). The gdc developers seem to have neglected to
put up all of the scary warnings that ldc had, so I used gdc. I
had to compile a patched gcc myself. It was actually not that bad,
though I have compiled gcc many times in the past.

But neither of these ports have all of the updates for D2. Then
you get to try to figure out whether the bug is in your code or in
the compiler. Ah, it brings back memories of tracking down C++
compiler bugs last millennium. Fun, it is not.

Also, I could never get the debugger to work. Grumble, grumble.

4) Documentation

There are no books on D and 'D' is a terrible search term. It would
be so much better if they had kept the original name 'Mars'. The
results you do find are scant and mixed between D1 and D2. The D
website pales in comparison to a good reference website like
cplusplus.com.

5) I could not get enums to work.

6) The standard IO module is in std.stdio. Why not std.io?


Different:
----------

7) If you do not specify a default case for a switch statement, it
throws an error.


C#
-------

The Good:
---------

1) Fast Compilation

It seemed about as fast as D. I got farther along so the code got
a bit larger than with D, but the compile time is only about 1
second for the whole project. Not enough time for me to browse
r.g.r.d.

2) Modules

Same as D. All good.

3) Reflection

Monodevelop can use reflection to browse dll's and give you the
objects, constants, and function signatures. This is really handy
since the documentation for libtcod's C# bindings seems to have
disappeared down the throat of a Sarlacc. Using reflection is way
better than looking at C++ header files cluttered up with
preprocessor directives, private members, inline functions, and
template implementations.

4) Bounds checking

While developing, automatic bounds checking caught a few bugs
early. I feel like I would have caught them in C++, but only after
a confusing segfault and some quality time with valgrind. Of
course, bounds checking may have contributed to poor performance...

5) Lambda functions

Being able to easily define a function for Find() is so much nicer
than having to define a function object. Not everything from
functional languages is bad.

6) Static Variables

Static variables are much more nailed down than in C++, so I do not
have to think about it as much. Less thinking == good.

7) Automatic operator overloads

If I overload operator+, I automatically get operator+=. Niiiice.


The Bad:
--------

8) Performance

The roguelike I am writing uses variable sized glyphs. This makes
pathfinding more complicated and CPU-intensive than a more
traditional roguelike. I also wanted to use a sound model, but
that ended up being far too CPU-intensive. So performance turned
out to be important.

It was very hard to reason about performance. I felt like I was
allocating a million little things on the heap that normally get
allocated on the stack. How does that impact performance? I don't
know!

The profiler was rather unhelpful. Half of the time is spent in
C++ code which does not get profiled. But that is OK, because, in
this case, I really only cared about the performance of the C#
code. Even with that concession, the two routines using the most
time were 'memset' with 17% and '/usr/bin/mono' with 15%.
Everything else was less than 10%. Thanks for nothing.

9) Memory leak

I got a serious memory leak that pretty quickly ate up all memory.
I was creating a Dictionary of DjikstraPath's. DjikstraPath has to
be Dispose'd, but I could not use 'using' because Dictionary's are
not Disposable. Disposing of each DjikstraPath independently
caused mysterious crashes. So I ended up using a different
algorithm. How I long for destructors.

10) Everything is a class

C# requires everything to be inside a class. There are no free
functions, and sometimes not everything is an object. For a far
more thorough and entertaining rant about this, you can read

http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html

11) Incremental compilation is unwieldy

It seems that I have to make the equivalent of a shared library in
order to get any sort of incremental compilation. I guess this is
the downside of not having to worry about circular dependencies.
This is not such a big deal since the compiler is so fast.

12) Files have to have namespaces

You have to have a namespace for each class. This is great if my
roguelike ever gets large enough to require this kind of partioning
(we all have such dreams...), but for small projects it is a bit
overbearing.

13) Debugging

I could not get the debugger mdb to work. So I got to spend far
too much time with System.Console.WriteLine (couldn't they have
made it a little shorter?).

14) I can not make my own 'built-in' type

I want a 2D Point object that uses natural notation. Something like

Pos p(1,1);
Pos q=p+Pos(3,2);

C# separates value based objects (structs) from reference based
objects (classes). So it would seem natural to use a struct. But
you still have to use 'new' when creating an instance of a struct.
This changes the previous example into

Pos p=new Pos(1,1);
Pos q=p+(new Pos(3,2);

Natural, it is not.

15) Equals is complicated

Overloading operator== means that you also have to overload !=,
isEquals, and HashCode. Really farkin' annoying.

16) Can not overload [] or ()

So to make a fancy vector class, you have to use unnatural notation
like array.at(x,y).

17) Emacs mode

The emacs csharp mode does not handle multiple case's in a row. It
indents the break in the wrong place.

18) No multimap or multiset

19) Non-Generic Collections

C# started with non-generic collections, which have now been
superseded by generic collections. It looks like it is time to
trigger Hejlberg's garbage collector.

20) Collections are limited

I have an event queue that stores @ and all of the monsters. It is
sorted by the time when they can next act. So it might look like

Time Entity
---- ------
2 B
2.2 @
2.3 d
2.3 c
4.5 A

'A' just cast a spell that takes a long time to recover, so it is
currently B's turn. Note that both 'd' and 'c' have the same time.
This simulates 'c' waiting for 'd' to act. For example, if 'd' is
blocking the corridor in front of 'c'.

'B' attacks @, which takes 1 unit of time. I have to move B back
in the queue after d but before A. I can implement the event queue
with a List. Then I move B by calling RemoveAt(0) and Insert(3,B).

But then I think that I want to try a LinkedList since they are
better at insertions and removals. But LinkedList does not take
RemoveAt() or Insert(). I have to call RemoveFirst() and AddAfter().

So I can not write a generic function that moves an element farther
back in a generic container. I have to specialize for each type of
container. It is as if the C# designers copied the good parts of
the STL, but left out the insanely great parts.

21) const and readonly too limiting

'const' can only be used for compile-time constants. 'readonly'
can be evaluated at runtime, but can only be used for fields in an
object. So there is no good way to enforce const'ness inside a
function. I guess I could construct an object that has a readonly
field. But give me a break, I just want "area=width*height" to be
constant.

22) Assert

After a fair bit of wrestling with compiler options, I still could
not get Debug.Assert() working under mono.

23) Difficult to split up the implementation

Big files are bad.

http://www.leshatton.org/Documents/Ubend_IS697.pdf

But if you have a class in a file, you have to implement all of the
member functions in that file. Unless you use partial classes.
But then classes can be expanded from arbitrary places. So if I
forget to compile in an implementation of a virtual function, it
will silently use the base class. No thanks.

24) False compiler alarm

The compiler exits with an *error* when it thinks that I am using
an uninitialized variable. This is very annoying when the logic is
a bit complicated but ensures that the variable will be
initialized. False alarms make me angry. You wouldn't like me
when I'm angry.

The Different:
--------------

25) Public

You have to specify 'public' for every public member. You can not
have a public section as in C++.

26) Delegates

Delegates are how C# handles function pointers. For what I have
done, they were just different. Not better, not worse.

27) Default initialization

Everything gets default initialized to 0. This seems wasteful, but
it is probably not that bad for a roguelike.

C++
---

The Good:
---------

1) Performance

After rewriting the code in C++, the pathfinding code ran 5 times
faster. Now I can use the good pathfinding algorithm, instead of
using some god-awful performance hack that makes players think that
the monsters are blind, deaf, and drunk.

2) Simple Classes

If I have a quick and simple class, I can just put it in a .hxx
file and not have to worry about adding it to the build system
(Assuming that the build system automatically checks dependencies.
Mine does, doesn't yours?).

3) Easy to separate the implementation

It is really nice to be able to put member functions into their own
files. So Foo::bar() goes into Foo/bar.cxx, Foo::baz() goes into
Foo/baz.cxx, etc. Simplicity.

4) Easy integration with C/C++

I am using libtcod (C with C++ interface) and Roguelikelib (C++),
and it is nice to not have to deal with any glue code to interface
with them.

5) Debugging

Valgrind is a magical tool that works unreasonably well at catching
all of the undefined behavior in C++ that people love to complain
about. Gdb has excellent emacs integration that makes bug stomping
almost too easy. Both of these tools work, and they work well.
Moreover, libstdc++ has a debug mode with a checked STL
implementation. I have never used it since valgrind+gdb always
found my bugs fast enough.

The Bad:
--------

6) Compile time

Compiling my code can take up to 4-5 seconds, depending on
optimizations. Change one file and it takes 0.8 seconds. Unless
it is a header file, in which case it goes back up. This is not
yet a large project, and I know from painful experience that it
will only get worse.

7) Multidimensional arrays

The built-in multidimensional arrays are not dynamically
resizeable. You can make your own, though you have to be careful.
Boost's multi_array, for example, is slower than molasses in
January.

8) Value vs Reference

When passing variables into a function, I definitely have to think
more about declaring them as "Foo foo" vs "Foo &foo" vs "const Foo
&foo". Remember: Less thinking == good.

9) Random syntactic annoyances

When defining classes, you have to have a semicolon ';' after the
closing curly brace. Also, you need include guards inside header
files. Finally, the syntax for passing pointers to member
functions is a bit obtuse, and the C++0X extensions do not really
help.

10) No lambda's ... yet

If I want to use for_each in C++, I have to write function objects.
No thank you. And no, boost::lambda is not going to cut it.
Someday I will upgrade to gcc 4.5 and get lambdas, but not today.
Range-based for would be great as well, but no one has actually
implemented them. Even so, having to write your own loops is not
so bad, especially with the 'auto' keyword.

11) Formatted strings

If I want to make a string with a number such as "You have 2 HP
left!!", I always end up using stringstream, and it always takes three
lines to do what should happen in one.

12) Headers are compiled at the same time as the main source files

When I make a major change that causes a lot of compilation errors,
I am constantly jumping around to bugs in different places. So I
never quite finish finding all of the bugs in one file before I
have to deal with bugs in another file.

13) References are difficult to use

References have to be bound at all times and there is no 'null'
reference. So you can not make an array of them and assign them
later. In the end, I just ended up using pointers.

14) Header files do not gracefully handle circular dependencies

You have to use forward declarations and split the code into
implementation files. This can get a bit convoluted, especially if
you want the code to be inline or a template.

15) Construction order

When writing a constructor, member variables are built in the order
that they are defined in the class. If you try to use a member
before it is built, it does not tell you. It just fails
mysteriously at runtime.

16) while(); {} vs while() {}

These two forms are both valid, but the first one does not execute
the inner braces of the loop. It is not too hard to get the first
one when you want the second.

The Different:
--------------

17) No garbage collector

Porting from D to C# was extremely straightforward. In contrast, I
had to modify the design of the C# code to get it to work in C++.
This included debugging a number of subtle issues. For example, it
is dangerous to use pointers as a unique id for an object, because
they can get invalidated when a vector grows. I ended up using
std::unique_ptr which solved some, but not all of the problems. I
feel that if I had just written the code in C++, I would not have
run into the subtle issues. Programming languages really change
the way you think.

I could have attached a garbage collector, but part of what I
wanted to find out is to what degree garbage collection affects
performance.

.



Relevant Pages

  • Re: "STL from the Ground Up"
    ... high-level intermediate language than can interoperate with many other ... If your language lacks expressive features then you cannot write code ... memory management in comparison. ... Mostly because type errors mean that the programmer and compiler disagree ...
    (comp.programming)
  • Re: A note on computing thugs and coding bums
    ... It would handle international characters if the execution character ... method I used in "Build Your Own .Net Language and Compiler". ... work areas and counting on Nul is an illusion. ...
    (comp.programming)
  • Re: access(FULLPATH, xxx);
    ... with "trial& error" to just silence the compiler. ... void *foo); ... given that the language in the specification _was_ abiguous and both ... documentation was paramount. ...
    (freebsd-questions)
  • Re: WaitForSingleObject() will not deadlock
    ... represent an incorrect implementation of the language. ... the *compiler* does not guarantee this. ... but to state it in terms of the execution instead of the formal semantics of the language ... as long as the optimizations do not change the semantics of the language). ...
    (microsoft.public.vc.mfc)
  • Re: Question: How can Lisp be both compiled and interpreted? does it use a virtual machine?
    ... Or, more generally, how can machine language, with its supreme power ... The answer is that the compiler does not produce arbitrary machine ... but machine code which sticks to certain rules regarding the ... only in locations known to the garbage collector, ...
    (comp.lang.lisp)