Skip to main content
GameDev.net gamedev.net
🔒 Locked

Going for a fast-prototype Lisp based language

Started by HairyTroll Dec 21, 2005 at 4:13 PM 29 replies 8.2k views
Original Post
HairyTroll
HairyTroll
I'm trying to come up with a rapid prototyping language that is easy for the Blitz crowd to understand. The code below shows a rather simple example. It renders a 10x10 matrix of cubes in OpenGL, assigns each cube a random color and then rotates each cube around the x, y, and z axis's by ~1 degree a second. In addition the viewer is able to pan, zoom and rotate the camera around these cubes. My question is, does your head explode when reading this code or can you get the basic flow and intent just by reading it, without any comments or documentation?

(defun rm-rotating-cubes ()
  (let ((width 320) (height 200)
	(button-state nil))
    (init-objects)

    (sdl::with-init ()
      (sdl::window width height :flags (list sdl::sdl-opengl))
      (rm::with-engine ((rm::get-hwnd) width height) root-node

	(let ((cubes nil))
	  (setf cubes (loop for i from 0 to 10
			    append (loop for j from 0 to 10
					 collect (new-mob :script (defscript ()
								      (rotate-mob actor 1.0 1.0 1.0))
							  :primitives (rm::new-box -1.0 -1.0 -1.0
										   1.0 1.0 1.0
										   (random 1.0) (random 1.0) (random 1.0)
										   :cx (* i 3.5) :cy (* j 3.5) :cz 0)))))
	  (add-actor root-node cubes :union t :compute-center t))
	(rm::set-default-scene root-node width height)

	(sdl:with-events
	  (:quit t)
	  (:keydown (state scancode key mod unicode)
		    (when (equal key 'sdl::SDLK-ESCAPE)
		      (sdl::quitevent)))
	  (:mousemotion (state x y xrel yrel)
			(rm::aux-handle-motion root-node button-state x y width height))
	  (:mousebuttondown (button state x y)
			    (setf button-state (rm::aux-handle-buttons root-node button x y width height)))
	  (:mousebuttonup (button state x y)
			  (setf button-state nil))
	  (:idle
	   (update-all)))))))
darookie
darookie
Even though I'm still trying to find some parts of my exploded head, I think it's a very nice expample.
A few parts I don't understand:

  • ((cubes nil)) why not (cubes nil)?

  • What's the 't'?

  • Also I just can't get used to identifiers with hyphens [smile].


HairyTroll
HairyTroll
Quote:
Original post by darookie

  • ((cubes nil)) why not (cubes nil)?

  • What's the 't'?

  • Also I just can't get used to identifiers with hyphens [smile].



Good questions :)

LET creates local variables. The reason why it is ((cubes nil)) and not (cubes nil) is because LET allows more than one variable to be defined at the same time. So, for example:

(let ((var-1 1)      (var-2 2)      (var-300 "String Here."))   (things that use the variables go here)   (here too))


As for the 't'. Well, (QUIT: ) is run whenever a quit event is received. And the code only exits when (QUIT: ) returns true. And in Lisp, T is true - actually, anything other that NIL is TRUE in Lisp, so I could as well have put (QUIT: 'EXITING-BYE-BYE). For this example, the application will exit on any quit event.
cypherx
cypherx
Hey HairyTroll,
Are you using any existing language or writing your own?

(plugging my own work)

I've been working on a lisp-based scripting language for the past month with maximal simplicity in mind. I've tried to clean up all the warts and onions, making things as easy for the unintiated as possible.

A few things I've changed:
- declare closures with 'fn' (instead of 'lambda')
- no setf, setq, only set
- all functions are generic methods (so you can overload without any hassle)
- overloaded operators for datatypes (+ works on numbers and vectors, no need for vec+)
- alternative 'let' syntax: (let (i 10) (...code...)) and (let ((i 10)) (...code..)) both work
- simpler loop constructs
(while [predicate] )
(foreach [item] in [container] [code)
(for [var] from [start-val] to [end-val] )
- easier syntax for optional and variable-number arguments:
(defun f (x [y 0] ... r) (list x y r))
Here y is an optional argument that defaults to 0, and r catches all
arguments passed after r.
- everything is an object, but there's no additional boilerplate OO code (like java or C++)
- a class based object system (so that C++ objects can be treated as lisp objects)
- An extremely easy C++ interface.

Most of my changes were inspired by goo (which has nice features but is horrible to read) and arc (which never materialized).

I think even with the above changes, lisp code looks messy and unstructured to the eye of a C++ programmer. I'd welcome any further suggestions to make the language easier to comprehend.

-Alex
HairyTroll
HairyTroll
Quote:
Original post by cypherx
Hey HairyTroll,
Are you using any existing language or writing your own?


I'm currently working in Emacs,SLIME and Lispworks.

Quote:
Original post by cypherx
I've been working on a lisp-based scripting language for the past month with maximal simplicity in mind. I've tried to clean up all the warts and onions, making things as easy for the unintiated as possible.


I'm hesitant to try and define a whole new language. I've been working on trying to abstract away as much of the game making fluff as I can. It would be great to have a C++ interface though. Right now most Lisps only interface to a C API.

It's a pity about Arc as I think Paul Graham really knows his stuff. I'll have to take a look at goo.

I do think that well thought-out Lisp code can be easier to read than C++, especially when templates become involved.

Kylotan
Kylotan
I would respond, but my head exploded.

I don't have anything constructive to say, except that I don't think LISP-like languages are at all easy to grasp if you only have a Basic/C++ sort of background. That's not a criticism of the language you have there, just a comment on the difference between that and what I normally use.
cypherx
cypherx
Kylotan,
I think LISP-ish languages are hard at first because you're missing all the visual anchors you depend on when reading a language with syntax. Much of lisp's power arises from all the code being represented as a list...but this can be nightmarish to read.

I think this is one of the biggest barriers to lisp's acceptance. It's not just "too many parens"...LISP requires you to read code differently (with less visual hints) than other languages.

That's why I'm trying to stuff as much syntactic sugar into my pseudo-lisp as possible. Underneath everything is a list, but it needn't look that way to the coder.

NoahAdler
NoahAdler
Are you aware of Fluxus? I don't use it myself, but I'm aware of it because of my interest in VJ and synthesis software. It's geared towards visual musical performance (via live coding) instead of game prototyping, but there's a lot of overlap between the two fields. It probably has a lot in common with what you're trying to accomplish here. Just thought I'id point it out for you, on the off chance that you're unfamiliar with it already.

-bodisiw
-bodisiw
flangazor
flangazor
My head doesn't explode when reading it but it's annoying having to figure out what the magic umbers are for. Abstract those into meaningful concepts and it will be nicer.

Also, in my experience, emacs tabbed code that gets an ugly distance horizontally can almost certainly be refactored into something cleaner (and left oriented).
HairyTroll
HairyTroll
Quote:
Original post by Anonymous Poster
Nope, it looks like Lisp to me with your own library bindings. Was there something revolutionary about this other than that it might be a Lisp that actually might really work?


Eh? Of course it works. It works perfectly fine right now and compiles and runs in Lispworks.

HairyTroll
HairyTroll
Quote:
Original post by flangazor
My head doesn't explode when reading it but it's annoying having to figure out what the magic umbers are for. Abstract those into meaningful concepts and it will be nicer.


You mean like:

(rm::new-box <x1> <y1> <z1>              <x2> <y2> <z2>              <r>  <g>               <x-pos> <y-pos> <z-pos>)


?
HairyTroll
HairyTroll
OK.... I've changes things a wee bit. The following code (yes, it runs just fine) will:

1) Initialize SDL
2) Initialise OpenRM (OpenGL Scene Graph Manager)
#2.1) Create an OpenGL rendering pipeline, and assign a default root node (root-node)
#2.2) Set up the default scene and camera.
3) Create a new 'cube' of a random color.
3.1) Specify that this cube should be rotated around the x, y and z axis's by 1 angle respectively each frame rendered.
#4) Handle events & message pump
#4.1) The exit event (user closes the window) terminates the game loop.
#5) Update the script (rotate the cube) each frame
#7) Render the OpenGL frame
#8) Swap the buffers
#9) Goto 4
#10) Upon exit
#10.1) Close the rendering pipeline
#10.2) Shut down and release resources for OpenRM
#10.3) Shut down SDL.

Of the steps above, only 1, 2, and 3 are performed by the coder, everything else is buried and handled automatically.

(defun rotating-cube ()  (let ((screen-width 320) (screen-height 200))    (sdl::with-init ()      (rm::with-engine (sdl::window screen-width screen-height :flags sdl::sdl-opengl) root-node           (add-actor root-node  (define-actor a-cube                                    (primitives (rm::new-box :x1 -1.0 :y1 -1.0 :z1 -1.0				                             :x1  1.0 :y1  1.0 :z1  1.0                                                             :r (random 1.0) :g (random 1.0) :b (random 1.0)                                                             :cx  0.0 :cy  0.0 :cz  0.0))                                    (script (defscript ()         	                               (rotate-actor actor :dx 1.0 :dy 1.0 :dz 1.0)))	   (rm::set-default-scene root-node screen-width screen-height)))))
flangazor
flangazor
Quote:
Original post by HairyTroll
Quote:
Original post by flangazor
My head doesn't explode when reading it but it's annoying having to figure out what the magic umbers are for. Abstract those into meaningful concepts and it will be nicer.


You mean like:

(rm::new-box <x1> <y1> <z1>              <x2> <y2> <z2>              <r>  <g>               <x-pos> <y-pos> <z-pos>)


?


No, i meant more like setting up a record structure (list, struct, vector) of coordinates to pass to the box construction. And then define it in the let statement.
HairyTroll
HairyTroll
Quote:
Original post by NoahAdler
Are you aware of Fluxus?


No, and thank you for the link. I'll definitely have a look at this.

cypherx
cypherx
Looks good, but the following things aren't obvious from the code:

* Why "with-init"/"with-engine" and not "init"/"engine"?

* What is the null list passed as the first argument to sdl::with-init?

* Where are 'root-node and 'actor defined?

* What's the point of calling "primtives" and "script" in add-actor?

-Alex

Quote:
Original post by HairyTroll
(defun rotating-cube ()  (let ((screen-width 320) (screen-height 200))    (sdl::with-init ()      (rm::with-engine (sdl::window screen-width screen-height :flags sdl::sdl-opengl) root-node           (add-actor root-node  (define-actor a-cube                                    (primitives (rm::new-box :x1 -1.0 :y1 -1.0 :z1 -1.0				                             :x1  1.0 :y1  1.0 :z1  1.0                                                             :r (random 1.0) :g (random 1.0) :b (random 1.0)                                                             :cx  0.0 :cy  0.0 :cz  0.0))                                    (script (defscript ()         	                               (rotate-actor actor :dx 1.0 :dy 1.0 :dz 1.0)))	   (rm::set-default-scene root-node screen-width screen-height)))))


The Reindeer Effect
The Reindeer Effect
Quote:
Original post by cypherx
* What is the null list passed as the first argument to sdl::with-init?


My guess is that sdl::with-init takes an association list that includes optional flags/settings/whatever ... it's pretty much standard stuff to either take an alist as an argument in this situation, or to have some weird plist "optional arguments" passed (this is common in the Common Lisp community, not recommended).
HairyTroll
HairyTroll
Quote:
Original post by The Reindeer Effect
Quote:
Original post by cypherx
* What is the null list passed as the first argument to sdl::with-init?


My guess is that sdl::with-init takes an association list that includes optional flags/settings/whatever


Half right. By default WITH-SDL is initialized using sdl::SDL-INIT-VIDEO which initializes the video subsystem. If I wanted to initialize both video and audio then I would do this: (sdl:with-init (sdl::SDL-INIT-VIDEO sdl::SDL-INIT-AUDIO) (STUFF-HERE))

HairyTroll
HairyTroll
Quote:
Original post by cypherx
* Why "with-init"/"with-engine" and not "init"/"engine"?


Good points, the current names do not make sense. I'll change these.

Quote:
Original post by cypherx
* What is the null list passed as the first argument to sdl::with-init?


sdl::wih-init can takes several parameters to initialize the various SDL subsystems. A null list means that SDL should be initialized using the default sdl::SDL-INIT-VIDEO which initializes the video subsystem. If I wanted to initialize both video and audio then I would do this: (sdl:with-init (sdl::SDL-INIT-VIDEO sdl::SDL-INIT-AUDIO) (STUFF-HERE))

Quote:
Original post by cypherx
* Where are 'root-node and 'actor defined?


root-node is the root node of the scene graph and is implicitly defined by RM:WITH-ENGINE. The scene graph root node must be bound to a variable that the programmer can refer to as it is used to define camera pointing etc. In this code RM:WITH-ENGINE binds the root node to 'root-node', but this variable can be named anything (e.g. top-node, node etc.)

ACTOR is is little more complicated, as it is defined as a side effect of DEFSCRIPT.

DEFSCRIPT does five things:

1) It creates a new class, for example (DEFSCRIPT (SIN-BOUNCE) (STUFF-HERE))
2) It creates the object initializer function
3) It creates the UPDATE function that the engine calls every frame to update the state of the script.
4) It creates a new instance of the class and returns that.
5) It uses symbol-macrolet to create references to all members of the class; things like ACTOR, TRIGGER, TIMESCALE, CURRENT-FRAME are all implicitly defined. See below for an example of a slightly more complicated rotate script [1].

Quote:
Original post by cypherx
* What's the point of calling "primtives" and "script" in add-actor?


:primitives; OpenRM allows many different types of primitives to be added to a single node; cubes, cylinders, quads, and triangle meshes can all be added to a single node. So, for example 1000 cube primitives can be added to a single node allowing all 1000 cubes to be translated or rotated using a single OpenGL command.

:script; The engine will execute the contents of :script once every frame.

Thanks for the comments.

[1] This script will rotate an object around an axis by x degrees a second:

(defscript (rotate)     (init ((dx 1.0) (dy 0.0) (dz 0.0)))  (rotate-actor actor dx dy dz))


To create a new rotate script and override the defaults, just do the following:

(NEW-ROTATE :DX 0.0 :DY 1.0)


The DEFSCRIPT macro actually expands into the following:

(PROGN  (DEFCLASS ROTATE            (SCRIPT)            ((DX :ACCESSOR DX :INITFORM 1.0 :INITARG :DX)             (DY :ACCESSOR DY :INITFORM 0.0 :INITARG :DY)             (DZ :ACCESSOR DZ :INITFORM 0.0 :INITARG :DZ)))  (DEFUN NEW-ROTATE (&KEY (DX 1.0) (DY 0.0) (DZ 0.0)) (MAKE-INSTANCE 'ROTATE :DX DX :DY DY :DZ DZ))  (DEFMETHOD UPDATE-ACTOR    ((ACTOR ACTOR) (SCRIPT ROTATE) ENGINE::TIMESCALE ENGINE::TRIGGER)    (SYMBOL-MACROLET ((ENGINE::CURRENT-TIME (ENGINE::CURRENT-TIME SCRIPT))                      (DX (DX SCRIPT))                      (DY (DY SCRIPT))                      (DZ (DZ SCRIPT)))                     (ROTATE-ACTOR ACTOR DX DY DZ)))  (NEW-ROTATE))

bboyBladeJ
bboyBladeJ
Hey guys,

I consider LISP a write-only language. Few years ago I implemented few non-trivial algorithms (such as finding shortest path in a graph, or solving the 9-puzzle problem) using LISP and I have to say it wasn't easy at all, but I made it work with a pretty clean code. However, a month after finishing, I realized I had troubles reading the code again. I think this can happen to everyone, who's not in touch with LISP every day.

I have a question I need to ask: Why exactly do you want to use LISP? LISP is a functional language and that's what it should be used for. Clean LISP style means no variable declarations, for-loops or anything resembling stuctured programming. These features are included in LIPS for same reasons as e.g. global variables in C++ i.e. you can always avoid using them, however in some cases they make your job a lot easier (I have never used any global variables in a C++ code). I believe using a scripting language would be a better choice than LISP.

Anyway, good luck with your experiment :-)

bboyBladeJ





HairyTroll
HairyTroll
Quote:
Original post by bboyBladeJ
LISP is a functional language and that's what it should be used for. Clean LISP style means no variable declarations, for-loops or anything resembling stuctured programming.


This is probably a common misconception concerning Lisp. Lisp is the Borg of programming languages. It allows functional programming, but you can also program in Lisp using GOTO's, structured programming, object orientated programming, aspect programming etc.

Learning the Lisp LOOP macro is a sure way to put hairs on your chest.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.