Option 9: A Shoot'em Up Miniature Video Game

Written by: Peter Keller (psilord at cs.wisc.edu)

Home Page Blog Home Lisp Software, Tutorials, Hacks

Description

Option 9 is a minigame I wrote in Common Lisp using cl-opengl and lispbuilder-sdl. It was a vehicle for learning CLOS. I've included a Theory of Operations section which describes the design, implementation, and postmortem, of this game.

Update 2015-06-25: This page is a little old. The Option-9 sources have moved well beyond what is described here (I added mines, fragmenting shots, redid the asset description DSL, etc) and I moved to cl-sdl2. I still use the old Opengl immediate mode API because it was a handy crutch so I could work on the logic of the game. After Option-9 progresses some more, I'll fix the Opengl API to be much more current.

Here's a screenshot. The shielded player is just to the lower right of the middle of the screen, has just fired a green diamond shot (called the SuperShot), and is using the Tesla Field weapon. Some of the enemies can not be affected by the Tesla Field weapon. Some enemies are firing.

Download

These are live sources. The HEAD is the development version. Version 0.7 is tagged with the "0.7" tag.

Here is the current location of option-9: git clone https://github.com/psilord/option-9

There is an option-9.asd file and the package is called :option-9.

License

Option 9 is licensed under the Apache License, Version 2.0.

Supported Platforms

Currently Option 9 operates with SBCL on Linux. Patches will be accepted for other lisp implementations, environments, or features people want to see. However, I only have resources to test on SBCL on Linux.

Contributions

After this document and source for version 0.1 was made public, there was some great feedback about it from comp.lang.lisp. These people helped me to better understand CLOS style and helped with other improvements. I've rewritten decent chunks of the code and this document to take what they said into consideration. Of course, any mistakes or problems in the style or code are strictly my own.

Zach Beane

Pascal Bourguignon

Pascal Costanza

Theory of Operations

This document describes the implementation and thought process behind version 0.7 of the Option 9 code base. It is also a postmortem for the 0.7 codebase where I reflect on what I've learned while explaining the code.

The main premise of this project was that I mainly wanted to learn CLOS and a little bit of cl-opengl and lispbuilder-sdl. I figured the best little project for me was a space shoot'em up game somewhere between Asteroids and Galaga. I did not intend to polish the game--only to make it somewhat fun to play. For example, it doesn't even have a splash screen or a help screen and the art is the minimum required. My main store of CLOS knowledge at the time of writing was Keene's Common Lisp Object System book. If I'm lucky, I learned something out of that book. I'm still learning Common Lisp and CLOS.

Game Design

A simple space shoot'em up game needs a few things (in no particular order):

A Player ship. It'll fire upwards, but be able to move all around the screen with the LEFT, UP, RIGHT, DOWN arrow keys.

The Player ship can fire a single shot straight up using the SPACE key.

Enemy ships. These fire downwards and can only move down while moving to the left or right.

Each entity in the game has maximum hit points, current hit points, How many score points it is worth, and how many damage points colliding with it may produce.

Differing kinds of weapons: Simple Shot: A shot which explodes when hitting anything else. Hardnose Shot: A shot which does not explode when hitting other shots of less hit-points than the damage points of the shot, but does explode when hitting a ship. Super Shot: A shot which does not explode when hitting other shots or other enemy ships as long as the damage level of the shot is greater than that of what it is hitting. Tesla Field Weapon: A passive weapon (meaning it can't be turned off) that looks like electric field lines snaking from a ship to other ships. the more lines which hit a ship, the more damage is done. It should be always active and can grow from a small range to a larger range as powerups are picked up for it. It should be visually cool. Proximity Mine: A mine which blows up when you get too close to it. Field Mine: A proximity mine that, if a ship has the Tesla Field weapon, will not be destroyed by the Tesla Field weapon and instead travel back along the field lines to the ship, damaging it.

Different shot shapes associated with each kind of ship and weapon.

Different kinds of shields for a ship: Shot Shield: Block shots only. Ship Shield: Block shots and other ships.

Different kinds of powerups. Powerups may hold more than one thing to bestow upon the player. They vanish after a period of time. Hardnose Shot Super Shot Extra hit points Shot Shield Ship Shield Tesla Field Weapon. It will take 1 powerup to get the weapon, and 4 more to power it up to the full range.

Both the player and enemies may have shields.

When an enemy dies, there is a chance a powerup or mine will appear where it died.

Some particle eye candy for the explosions.

Just enough of an AI for the enemies to make the game fun. The AI for each enemy should be able to have as input the game state. The AI controls if an enemy decides to shoot.

Enemies should automatically spawn after a random time.

Some kind of a text console so I can debug or alter the game while it is in progress. The console should be brought up with the E key on the keyboard.

If the player dies, then the score goes to zero and the player respawns.

The game should be pausable by hitting the P key on the keyboard.

An asset data file holding the geometry information along with whatever else the game needs. It should be read upon start of the game.

The game can be quit at any time by pressing the Q key.

The game will run at 60fps and use simple physics based upon frames per second.

Each time the player fires one point is deducted from their score.

There will be a high score on the upper left of the screen which only increases if it is surpassed. It starts at zero.

On the upper right side of the screen is the current score. It also starts at zero.

Where do we go from this design? There are five natural groups which fall out of the above: managing the assets, the game state, the individual object's state, the simulation of the world, and the game debugging/manipulation environment.

Predictably, these aren't as well separated as one would like. For example, the entities know about the game state and the game state knows how to manipulate entities. I'll try and cut a straight of path as I can through the source. All code in this project is at maximum safety and debuggability.

The Global State

Here is the entirety of the contents of the file package.lisp. Special variable *game* is used to track the game state. Special variable *all-entities* is used to track all of the entities I read out of the asset file. These are special since it allows me access to them via the text console which I will describe much later. The slightly peculiar use of *id* is so that when the game is running, ids for new objects start at 0. If I had just placed the new-id function into a closure, then each time I'd run the game--unless I reloaded this file, the ids would start up where they left off. I didn't want that behavior. We use the euclidean-distance in various places in the code, so we'll include it early.

The contents of package.lisp.

(defpackage #:option-9 (:use #:cl) (:export #:option-9)) (in-package #:option-9) (declaim (optimize (safety 3) (space 0) (speed 0) (debug 3))) (defvar *game*) (defvar *all-entities*) (defvar *id*) (defun new-id () (let ((id *id*)) (incf *id*) id)) (defun euclidean-distance (fx fy tx ty) (let ((factor-1 (- tx fx)) (factor-2 (- ty fy))) (let ((ndist (+ (* factor-1 factor-1) (* factor-2 factor-2)))) (values (sqrt ndist) ndist))))

Entity Description

I'm going to use the word entity to mean both an instance (especially in the game world) of any concrete class in the class hierarchy in addition to the actual entity class. The context should make it clear which one I mean.

We come to the description of the entities in the game. Since I'm learning CLOS and interested in how inheritance, generic methods, etc all work, this is probably a slightly over engineered class hierarchy. In the figure below, the capitalized title of each node is the class name. The fields underneath the class name are read/write accessor function names for slots which hold that piece of information. The actual slot names themselves are named very similarly, but intentionally different, to its corresponding accessor function name. The purpose for this is so if I ever export the symbol used for the accessor name, I don't also export the symbol used for the slot. This makes it harder to use with-slot with the instances. If no slots are specified in that class the entry is left blank. I included all class definitions in the option-9 game in this image.

The dashed nodes are parental base classes which I do not instantiate in the game. The solid nodes are the concrete classes which I do instantiate. Arrows point towards the direct super classes of a class. There are no shared slots in this hierarchy. If you're interested in how I made this image, I used dot from the graphviz set of tools and here is the dotfile.

The particular breakdown of classes for a game entity was chosen to make effective use of multi-methods in CLOS. After experiencing multi-methods, I would gladly pay the small cost it takes to implement them in the runtime since it makes writing the simulation code much more straightforward and concise. It just feels more natural to me than equivalent design patterns in other languages. I found it takes less code to write my intention and less code is better. I spent much more time in the code base to deal with the simulation of the game world objects, so certain classes like the GAME class and whatnot are somewhat neglected in their design.

Let's see what this looks like in the code itself. Each slot gets an obvious :initarg, a default value with :initform, and a basic read/write accessor with :accessor. The code which describes all classes in the option-9 game are found in the file classes.lisp.

Class Entity

This class holds fundamental things which all game objects need. Namely an id number, view of the game state, the maximum number of it points this entity may possess, its current hit-points, if something collided with this entity, how many damage points would it produce, the number of points the entity is worth for score, is the status :alive, :dead, or :stale, the charge of the entity (used for the Tesla Field weapon), how many sparks does it make when it explodes, and how many more additional sparks for random effect should be present? One curious slot, however, is %auto-finish-construction. This is an artifact of how I load the assets file and construct instances of various classes in the game. I'll explain later what it means and why it is there.

(defclass entity () ((%id :initarg :id :initform (new-id) :accessor id) (%game-context :initarg :game-context :initform nil :accessor game-context) (%max-hit-points :initarg :max-hit-points :initform 10 :accessor max-hit-points) (%hit-points :initarg :hit-points :initform 10 :accessor hit-points) (%damage-points :initarg :damage-points :initform 10 :accessor damage-points) (%points :initarg :points :initform 0 :accessor points) (%status :initarg :status :initform :alive :accessor status) (%charge :initarg :charge :initform -1 :accessor charge) (%initial-sparks :initarg :initial-sparks :initform 10 :accessor initial-sparks) (%additional-sparks :initarg :additional-sparks :initform 50 :accessor additional-sparks) ;; Unless we specify otherwise, we always try to run whatever ;; finishing constructor work we need to on a class by class basis. (%auto-finish-construction :initargs :auto-finish-construction :initform t :accessor auto-finish-construction)) (:documentation "The Entity Class"))

Class Ephemeral

This class simply holds how long in time something will live. There is a maximum duration of life, plus a countdown. In option-9, if an entity is ephemeral (defined by a non-nil ttl) and the ttl reaches 0, the status becomes stale. This means the entity cannot participate in the game world. It will be removed from the world at the earliest possible moment in the game loop.

(defclass ephemeral () ;; temporal simulation variables ((%ttl :initarg :ttl :initform nil :accessor ttl) (%ttl-max :initarg :ttl-max :initform nil :accessor ttl-max)) (:documentation "The Ephemeral Class. Used for things which need a temporal time limit"))

Class Location

This class describes physical locations and their directions of movement. Not only does it participate in the Entity class hierarchy, but also I use it standalone to record vector fields.

(defclass location () ;; physical simulation variables ((%x :initarg :x :initform 0 :accessor x) (%y :initarg :y :initform 0 :accessor y) (%dx :initarg :dx :initform 0 :accessor dx) (%dy :initarg :dy :initform 0 :accessor dy)) (:documentation "The Location Class. Used to hold the current position and direction vector at that position"))

Class Frame

Each entity in the game must have a physical and temporal connection to the game. This class combines those concepts into one idea.

(defclass frame (ephemeral location) () (:documentation "The Frame Class"))

Class Shape

This class holds all of the geometry information for an entity in the game. For this game, the geometry is extremely simple. The %primitives slot consists of lists opengl primitives which contains lists of vertex/color pairs. There are no textures or sprites in this game. Color interpolated polygons or lines are possible, but not used. If I wanted to have animated frames, or such things as textures, this is where they would exist.

(defclass shape () ((%primitives :initarg :primitives :initform nil :accessor primitives)) (:documentation "The Shape Class"))

Class Drawable

Finally we get to what it means to be a drawable thing in the game. All this class does is inherit form the direct superclasses and provide one interface to the three distinct pieces of functionality.

(defclass drawable (entity frame shape) () (:documentation "The Drawable Class"))

Class Collidable

Some drawable objects may collide with each other during their simulation in the game world. This class represents the information needed to determine collisions between drawables. Option 9 uses a very simple collision system based upon the distance of the centers of two objects in world space from each other. This means that sometimes the edge lines of two objects may pass through each other if the geometry of the object extends beyond the collision radius. For my purposes, this is fine.

(defclass collidable (drawable) ((%radius :initarg :radius :initform 0 :accessor radius)) (:documentation "The Collidable Class"))

Class Digit

Digits are just objects which are a vector font for the numbers zero through nine. They can't collide with anything and are just drawable.

(defclass digit (drawable) () (:documentation "The Digit Class"))

Class Spark

Sparks are the explosion detritus. I suppose from a technical point of view sparks and digits really ought to be one object since they have the same behavior. The reason it is like this is because originally I had some different effect ideas for sparks which haven't yet appeared in the source codes. If I come back to it, I'll likely add some stuff to sparks and then they'll be obviously different.

(defclass spark (drawable) () (:documentation "The Spark Class"))

Class Brain

The Brain class is the means by which the AI is implemented. In a game simulation step, when %until-next-action becomes zero, the game entity may think about what it wants to do given the game state. The AI is VERY simple in Option 9. It is only a stub suitable enough to have an enemy fire a shot at the player. Real AI would involve much more work in the thinking methods.

(defclass brain (collidable) ((%until-next-action :initarg :until-next-action :initform 0 :accessor until-next-action)) (:documentation "The Brain Class"))

Class Powerup

Funnily enough, I was playing extremely early versions of the game and getting seriously annoyed that I was dying so often. I needed some heavier firepower and shields. Well, where does one get such things? That's right, powerups! One might be wondering why I have different shot types, different shield types, but only one powerup type. Surely one can have powerups for different kinds of shots, shields and other things.

The reason is that the behavior of all of the powerups are identical. They just act to transmit their contents to the ship which hit them. Their contents can be a main-gun, a passive-gun, a shield, or some health points. In Option 9, powerups only hold one thing in practice. The engine doesn't prevent me from having more things that a powerup can have at once. Some future version of this game, if any, may capitalize on this.

(defclass powerup (brain) ((%main-gun :initarg :main-gun :initform nil :accessor powerup-main-gun) (%passive-gun :initarg :passive-gun :initform nil :accessor powerup-passive-gun) (%main-shield :initarg :main-shield :initform nil :accessor powerup-main-shield) (%health-level :initarg :health-level :initform 0 :accessor powerup-health-level)) (:documentation "The Powerup class"))

Class Ship

Finally! Now we're getting somewhere. Here is a ship. It has a main-gun, a passive-gun, and a shield. If this game were to be hacked upon later, I'd likely add secondary missiles, shields, or other goodies. Notice that the ship HAS-A gun and HAS-A shield from an OOP point of view. This perturbs how to resolve a collision event between entities in the game when coupled with the idea of multiple dispatch. I think I ended up with a decent solution which I'll describe when I get to that part.

(defclass ship (brain) ((%main-gun :initarg :main-gun :initform nil :accessor ship-main-gun) (%passive-gun :initarg :passive-gun :initform nil :accessor ship-passive-gun) (%main-shield :initarg :main-shield :initform nil :accessor ship-main-shield)) (:documentation "The Ship Class"))

Class Player

The reason why players and enemies are not simply instances of class Ship in the game state is that I can utilize multi-methods to determine what happens when player objects and enemy objects collide. If I just realized ALL ships as the class Ship, then I'd have to mark them internally as either player or enemy and manually dispatch on what to do with them when they collide. Having them be separate types allows me to leverage CLOS with good effect.

(defclass player (ship) () (:documentation "The Player Class"))

Class Enemy, Enemy-1, Enemy-2, Enemy-3

These classes exist to allow multi-methods to work effectively. It becomes easy to resolve collisions or have different AI algorithms based upon enemy type.

(defclass enemy (ship) () (:documentation "The Enemy Base Class")) (defclass enemy-1 (enemy) () (:documentation "The Enemy 1 Class")) (defclass enemy-2 (enemy) () (:documentation "The Enemy 2 Class")) (defclass enemy-3 (enemy) () (:documentation "The Enemy 3 Class"))

Class Weapon

The weapon class is the progenitor to all weapons in the game.

(defclass weapon (brain) () (:documentation "The Weapon Class"))

Class Shot, Simple-Shot, Hardnose-Shot, Super-Shot

These classes exist to allow multi-methods to work effectively. It becomes easy to resolve collisions between objects and especially in the context of shields.

(defclass shot (weapon) () (:documentation "The Shot Class")) (defclass simple-shot (shot) () (:documentation "The Simple Shot Class")) (defclass hardnose-shot (shot) () (:documentation "Shots which aren't destroyed by bullets!")) (defclass super-shot (shot) () (:documentation "Shots which aren't destroyed by ships!"))

Class Mine, Proximity Mine, Field Mine

(defclass mine (weapon) () (:documentation "The base mine class")) (defclass proximity-mine (mine) () (:documentation "The Proximity Mine Class")) (defclass field-mine (mine) () (:documentation "The Field Mine Class"))

Class Shield, Shot-Shield, Ship-Shield

A shot-shield protects the ship ONLY against other shots, but a ship-shield protects both against shots and other ships (destroying both upon contact). I never resolved what happened when two ship with shields hit each other, so for now the ship-shield (which only the player can get) destroys the other ship regardless if it shielded or not.

(defclass shield (brain) ((%shots-absorbed :initarg :shots-absorbed :initform 5 :accessor shots-absorbed)) (:documentation "The Shield Base Class")) (defclass shot-shield (shield) () (:documentation "The Shot Shield Class")) (defclass ship-shield (shield) () (:documentation "The Ship Shield Class"))

Class FieldPath, PathContact, Field

Overview: The Field class represents the subset of an electric field--as defined by physics, between an entity being viewed as a charge and the other participating entities (also being viewed as charges). The subset of the electric field is the computed FieldPaths which trace an electric field line from the source to wherever it ends up (either hitting an opposite charge or the edge of the world where it gets clipped). The PathContact class represents when a field line collides with an entity of the opposite charge. It collates the one or more fields paths which touch a particular entity.

FieldPaths record the number of steps they went while tracing the electric field line through the field. A record of each location found during the trace is kept.

(defclass fieldpath () ;; How many steps the path went before it either hit something or ;; reached the end of its range. This is in world space. ((%steps :initarg :steps :initform 0 :accessor steps) ;; The vector containing the location coordinates of each step with ;; element 0 being the start of the path. (%path :initarg :path :initform nil :accessor path)) (:documentation "The Field Path Class"))

PathContacts are used as values in a hash table where they keys are either an entity id or the key :no-collision. They record how many and which field paths contacted an entity. This is used later when we compute the collision of the a passive field weapon to all participating entities. In reality, fields "collide" with everything all the time. However in Option-9, we only determine if FieldPaths collided with participating entities. For paths whose key is :no-collision, it just means that the path exists, it just didn't hit anything.

(defclass pathcontact () ((%number-of-contacts :initarg :number-of-contacts :initform 0 :accessor number-of-contacts) (%path-ids :initarg :contacts :initform nil :accessor path-ids)) (:documentation "The Path Contact Class. This is stored on a per entity basis and records the field path-ids that touch that particular entity."))

The Field class has a range which determines how the number of total steps out the field paths may travel. The num-paths are how many field paths are to be traced. In Option-9 the origin of the traced field paths are distributed evenly around the originating ship. The FieldPaths are kept in the paths array and each FieldPath is identified by its index in the path array. Each time a field line contacts an entity, it is recorded in the entity-contacts hash table. This allows us to later determine if the field collided with a particular entity by looking up that entity in the entity-contacts hash table. If found, action may be taken. The paths array and entity-contacts are regenerated each frame.

(defclass field () ;; This range is described in the number of steps I should follow ;; the field line trace. ((%range :initarg :range :initform 1 :accessor range) ;; Num paths are how many even distributed paths should be followed ;; from around the field generating object. (%num-paths :initarg :num-paths :initform 1 :accessor num-paths) ;; An array of fieldpath classes where each one is a trace of the field ;; line in world space. (%paths :initarg :traces :initform nil :accessor paths) ;; A hash table of pathcontact classes keyed by the entity id the trace ;; touches, or "no-id" if it doesn't touch. (%entity-contacts :initarg :contacts :initform (make-hash-table :test #'equal) :accessor entity-contacts)) (:documentation "The Field Class"))

Class TeslaField

The Tesla Field is a weapon built from the field class and the weapon class. It implements a more useful interface to a field for the purposes of Option-9 and allows us to determine what it means to for an entity to collide with the field and what actions to take. A thing to note is that while %power-range and %power-lines might seem identical to the Field's %range and %num-paths, they are not. They are related by a function amenable to the number of powerups acquired while playing Option-9, and how far one steps or how many total paths there are in the field. The order of parentage is important since I want to pick certain methods specialized on the field class over those specialized for the weapon class.

(defclass tesla-field (field weapon) ;; This is a quantized range of power for the tesla-field ((%power-range :initarg :power-range :initform 1 :reader power-range) (%power-lines :initarg :power-lines :initform 1 :reader power-lines)) (:documentation "The Tesla Field Class"))

Class Game

The Game class holds all of the visible game objects in their various sets, the score and score boards, and a timer for when to spawn enemies. The game world consists of a real number space where the width is 0 to 1 left to right and the height is 0 to 1 bottom to top. Any world coordinates are in that space. Each kind of object is kept in its own list which makes collision detection between objects easier to control. I do not want enemies, or their shots, to collide with each other, for example.

It is class although there are no generic functions for it other than those defined with :accessor. It could just have easily been a structure but I wanted more practice with CLOS.

In retrospect, I see now that if I added an add-entity generic function whose methods, based upon single dispatch on the instance type, placed the entity into the right list, it would have been better. The reason why it isn't like that is because I created this class very early in the game and the knowledge I learned about CLOS while writing the rest of it wasn't in my mind yet. I could have just changed the source and never mentioned it at all. But then, for those still reading along who haven't killed themselves out of boredom, you wouldn't have seen a moment of comprehension and synthesis of the knowledge. In fact, I would go so far as to make player-shots and enemy-shots mixins with the shot class so I can use that to put them into the right sets in the game class.

Another choice I could have made in creating the game state was that a hash table could have held lists of objects keyed by the name of the object set. I really went back and forth with that concept and figured that since arbitrarily typed objects aren't being created out of nothing at runtime I didn't quite need that particular abstraction. With the current solution, each time a new concrete object type shows up I have to tinker with a bunch of source. In this small game it is ok and the code maintenance scaling factor allowed by the hash table solution would seem a bit overkill. If I start adding many more types of objects which need to all live in their own collision sets, then I'd likely reify the collision sets into real objects, shove the entities into the aforementioned hash table, and use a data-driven approach for computing the collisions between the sets.

;; Each thing in the world is kept is its particular list. This makes it ;; easy to perform collision detection only as necessary. (defclass game () ((%players :initarg :players :initform nil :accessor players) (%player-shots :initarg :player-shots :initform nil :accessor player-shots) (%enemy-mines :initarg :mines :initform nil :accessor enemy-mines) (%enemies :initarg :enemies :initform nil :accessor enemies) (%enemy-shots :initarg :enemy-shots :initform nil :accessor enemy-shots) (%sparks :initarg :sparks :initform nil :accessor sparks) (%power-ups :initargs :power-ups :initform nil :accessor power-ups) (%score :initarg :score :initform 0 :accessor score) (%score-board :initarg :score-board :initform nil :accessor score-board) (%highscore :initarg :highscore :initform 0 :accessor highscore) (%highscore-board :initarg :highscore-board :initform nil :accessor highscore-board) (%enemy-spawn-timer :initarg :enemy-spawn-timer :initform 60 :accessor enemy-spawn-timer) (%paused :initarg :paused :initform nil :accessor paused)) (:documentation "The Game Class"))

Loading Entities from the Asset File

While I could simply make any of the above classes with CLOS' make-instance, I wouldn't get very useful classes since they would only hold the defaults. For things like the Shape class slots, having NIL as your primitives would make a pretty game--if you were only interested at looking at a black screen while playing it.

Before I get to the format of the entities in the asset file, I'll make mention of how make-instance works. You call make-instance with a symbol representing the class name and a list of initargs with values that then get assigned to the local slots of the created instance. Knowing this, why don't we take advantage of Lisp's syntax and specify each entity in our asset file with a :kind, a :class, and :initargs. To be more clear:

;; An example entry in the entities section of the option-9.dat file. ((:kind :player-simple-shot) (:class simple-shot) (:initargs :radius .04 :primitives ((:line-loop ((0 1) (1 1 1)) ((1 0) (1 1 1)) ((0 -1) (1 1 1)) ((-1 0) (1 1 1))))))

The above is an alist with three keys: :kind, :class, and :initargs.

The cdr of the :kind list is another keyword which must be unique to each realized entity in the asset file. We need it unique because we'll be storing this information into a hash table keyed by the :kind value.

The cdr of the :class list is the symbol name of a CLOS class as defined with defclass. In practice, this will be one of the concrete classes. When we ultimately call make-instance, this is the class instance we'll need to create for this entity.

The cdr of the :initargs list is EXACTLY the list that one would supply to make-instance to create an instance of the class. This list is not evaled, however, so there can't be arbitrary lisp expressions there as values for the class' initargs.

All entity entries are placed into a single form in option-9.dat whose first element is :entities. This mildly simplifies reading the file.

;; The containing form for all entities in option-9.dat (:entities <entity-1> <entity-2> <entity-3> ... <entity-N>)

Here is the code which reads the entity entries out of the asset file into the *all-entities* hash table. Currently there is only one form in the asset file and it is the :entities form described above. We take careful note of how to find the asset file so it is based off of the installation directory of the package.

;; This takes a relative filename based at the installation location ;; of the package. (defun load-all-entities (filename) (let ((entity-hash (make-hash-table :test #'eq)) (entities (with-open-file (strm (asdf:system-relative-pathname :option-9 filename) :direction :input :if-does-not-exist :error) ;; Read the symbols from the point of view of this package ;; so later when we make-instance it'll work even if the ;; user only "used" our package. (let ((*package* (find-package 'option-9))) (read strm))))) (assert (eq (car entities) :entities)) (loop for i in (cdr entities) do (setf (gethash (cadr (assoc :kind i)) entity-hash) i)) entity-hash))

The only odd thing in there is the find-package call. Since we defined the classes in the option-9 package and the user might have called use-package to load the game, we need to intern the class symbol from the asset file into the option-9 package when we (read ...) the data file and not in whatever other package the user might be in. To implement this, we find the 'option-9 package and temporarily rebind *package* to it. While not exactly flexible, it is good enough for this. Oh, and there isn't much error checking, sorry.

Constructing Entities

Now that we've loaded the entities, how do we use this information and construct an instance of one of the concrete classes? What does the constructor for an entity look like and how does it relate to the *all-entities* hash table we constructed? The answer is in two parts. Part one is a function called make-entity which looks up the kind of object we want in *all-entities* and calls make-instance with the class name symbol and initargs. Part two will be that once the object is finished being constructed with make-instance we might have to do a pass over the object to complete the construction in terms of being useful in the game.

Part One of Entity Construction

The function make-entity will construct the entities we need. First it gets the value associated with the kind value supplied to the function out of the *all-entities* hash. The value is exactly the entity form read from the asset file. Then we call make-instance with the desired class using the cls variable. The ANSI Common Lisp specification states that initargs passed to make-instance are consumed in a left to right order and if an initarg is specified multiple times, only the first one is used. Knowing this, we append the initargs list to the passed in override-initargs list to ensure that we can override any initargs from the asset file.

Notice how advantageous it was to store the initargs in the same form we'd use to call make-instance. This allowed us to use the function apply with great simplicity. The explanation of to what *game* is bound will happen much later.

;; A factory constructor to make me any object of any kind read into ;; *all-entities*. Each entity also knows the game context in which it ;; will be placed. This allows the generic methods of entities to ;; inspect the game universe or insert effects into the game like ;; sparks. (defun make-entity (kind &rest override-initargs) (multiple-value-bind (info present) (gethash kind *all-entities*) (assert present) (let ((found-kind (cadr (assoc :kind info))) (cls (cadr (assoc :class info))) (initargs (cdr (assoc :initargs info)))) (assert (eq found-kind kind)) (assert cls) (make-instance-finish (apply #'make-instance cls :game-context *game* ;; The values of the override arguments are accepted ;; first when in a left to right ordering in the ;; argument list in concordance with the ANSI spec. (append override-initargs initargs))))))

However, there is that curious make-instance-finish function. What does that exactly do with the returned instance from make-instance?

Part Two of Entity Construction

There is a small chicken and egg problem with constructing an object in the system we've designed. Let's suppose we construct an entity which has specified :ttl-max in the asset file. How can make-instance set up the :ttl slot of the object if one of the arguments to make-instance is specifying the :ttl-max itself? There is probably a right answer to this question concerning CLOS, but I don't yet know it. Instead I'll explain how I solved it.

What I did was create a generic function called make-instance-finish. This generic function can be specialized to finish the completion of any object which needs additional internal slots set up after the instance has been constructed. Here is the code relating to make-instance-finish and the generic methods for each class type which needs additional work done. This is found in source file methods.lisp. The primary method of make-instance-finish does nothing but return the instance. I alter any behavior with before or after methods.

Notice for a powerup, I allow the setup of whatever %ttl there should be, but I add a constant second to it if there is a %ttl at all. This is done with an after-method. This is because I got tired of not having any time at all to grab the powerup. :)

Notice with the generic method for enemy-3, I implement the particular algorithm as a before-method. 75 percent of the time, we set the main-shield to nil and when this happens, the next generic make-instance-finish method on the Ship class will do nothing. In a more developed game, there would be a few more knobs to control exactly how and when enemy-3 might not have its shield.

The very curious implementation of make-instance-finish for the tesla field is due to my defining a setf method on those two accessors. The setf method takes the initial power-range and power-lines, set by :initargs from the option-9.dat file, and in the act of setting them back to their same value, sets up the rest of the instance based upon those numbers.

;; In general, we don't do anything special to any object that we create. (defmethod make-instance-finish (nothing-to-do) nothing-to-do) ;; If any random entity sets a ttl-max and nothing more specific changes this ;; method, then assign a random ttl based upon the ttl-max. (defmethod make-instance-finish :after ((s ephemeral)) (when (not (null (ttl-max s))) (setf (ttl s) (random (ttl-max s)))) s) (defmethod make-instance-finish :after ((e entity)) (when (auto-finish-construction e) (setf (hit-points e) (max-hit-points e))) e) ;; A powerup's ttl is the random amount up to ttl-max PLUS a constant second (defmethod make-instance-finish :after ((p powerup)) (when (auto-finish-construction p) (when (ttl p) (incf (ttl p) 60))) p) ;; Ships that specify a main-shield via keyword need to have them converted ;; to realized shield objects. (defmethod make-instance-finish :after ((ent ship)) (when (auto-finish-construction ent) (when (ship-main-shield ent) (setf (ship-main-shield ent) (make-entity (ship-main-shield ent)))) (when (ship-passive-gun ent) (setf (ship-passive-gun ent) (make-entity (ship-passive-gun ent))))) ent) ;; For enemy-3 there is only a 25 percent chance that it actually has ;; the shield specified in the option-9.dat file. If we decide it ;; shouldn't have a shield, the we set the main-shield to null which ;; makes the above generic method a noop. (defmethod make-instance-finish :before ((ent enemy-3)) (when (auto-finish-construction ent) (when (<= (random 1.0) .75) (setf (ship-main-shield ent) nil))) ent) (defmethod make-instance-finish :after ((ent tesla-field)) (setf (power-range ent) (power-range ent)) (setf (power-lines ent) (power-lines ent)) ent)

However, there is a choice of implementation available to me concerning enemy-3. I could have implemented the idea as an after-method, in which case the shield instance will always be created and 75 percent of the time I'll throw it away and set it to NIL. I didn't like this method because I always have to pay the cost of constructing an instance that I won't use 75 percent of the time. I also could have used an around-method where 25 percent of the time I (call-next-method) and 75 percent of the time I just set the main-shield back to nil and do nothing else. I didn't like the around-method as much because if setting up a shield (or lack thereof) gets more complicated, I'd miss out on all of that behavior when I don't call-next-method. To see what I mean, maybe in the future I'd implement a special secondary shield on a ship which can only be active when the main one is inactive. Using an around-method, I can set the main shield inactive, but then there is no machinery to turn on the secondary shield unless I specifically call get-next-method. See how the future modularity of the code broke a little bit when I hypothetically used an around-method in that case? There is a case later in the source with an around-method that I do think is ok. In that case, the around-method really does stop the behavior of the more general methods.

The Entity Protocol

Now that we can finally make the entities in the game, we can talk about the generic methods that deal with those objects. This is where the game design itself is mostly implemented. A protocol is the definition of behavior for an object hierarchy--in essence, the "verbs" to the objects "nouns". We've already seen one generic function in the entity protocol, make-instance-finish, here are the others:

(defgeneric sparks (entity) (:documentation "How many sparks the entity produces when it dies")) (defgeneric mark-dead (entity) (:documentation "This marks an object as dead.")) (defgeneric deadp (entity) (:documentation "Returns T if the object is dead.")) (defgeneric mark-stale (entity) (:documentation "This marks an object as stale (which means out of bounds or the time to live expired.")) (defgeneric stalep (entity) (:documentation "Returns T if the object is stale")) (defgeneric mark-alive (entity) (:documentation "This marks an object as alive. Isn't called right now since object default to being alive when they are created.")) (defgeneric alivep (entity) (:documentation "Returns T is the object is alive.")) (defgeneric die (entity) (:documentation "Performs the necessary actions when an entity dies")) (defgeneric distance (left-frame right-frame) (:documentation "Computes the distance between the origins of two frames")) (defgeneric active-step-once (frame) (:documentation "Performs one step in the simulation of this entity. By default it will move the x y location of the entity by dx dy in the frame and will decrease the ttl towards zero if present. It is intended that before or after methods are used in more specific objects to take advantage of the simulation step.")) (defgeneric passive-step-once (entity) (:documentation "Performed after all active-steps, this performs on passive step in the simulation of the entity. A passive step is something which requires final knowledge of all entity locations. An example is field generation.")) (defgeneric render (drawable scale) (:documentation "Renders the entity shape with respect to the frame at the scale desired.")) (defgeneric collide (left-collidable right-collidable) (:documentation "If the left and right entities collide, then invoke their perform-collide methods.")) (defgeneric perform-collide (collider collidee) (:documentation "Perform whatever effects need to happen now that it is known this entity collided with something.")) (defgeneric damage (thing other-thing) (:documentation "Two things _may_ damage each other if they interact")) (defgeneric explode (thing) (:documentation "When something dies and needs to explode, this is how it explodes.")) (defgeneric shoot (ship) (:documentation "The ship, be it player or enemy or something else, shoots its main gun.")) (defgeneric absorbs (collider shield) (:documentation "Should return true if the shield absorbs a specific collider")) (defgeneric think (brain) (:documentation "For entites which need to think, count down until the next idea shows up and when it does, invoke it")) (defgeneric idea (brain) (:documentation "If anything needs to be done about a future or current action to take, this is where it is done.")) (defgeneric contacts (contacter contactee) (:documentation "Different than collide, this return a data structure explaining the manner of the contact between the concacter and the contactee.")) (defgeneric generate (field source participants) (:documentation "Certain types of effects like fields need a generation phase which computes their solution in terms of a source of the effect and other entities (which may also include the source) on the board.")) (defgeneric increase-density (tesla-field) (:documentation "Increases the number of lines traced from the source of the field or as appropriate for whatever class this gets applied to.")) (defgeneric increase-power (item) (:documentation "Increases one of multiple aspects of the object that represents its power. It does so randomly but intelligently such that if one aspect is already at max, another is chosen.")) (defgeneric increase-range (item) (:documentation "Increases the range of the item.")) (defgeneric power-density-maxp (item) (:documentation "Returns true if the item is at the maximum density (of whatever quantity)")) (defgeneric power-range-maxp (item) (:documentation "Returns true if the item is at the maximum range (of whatever quantity)")) (defgeneric trace-field-line (field path-num tx ty q1 charges) (:documentation "Trace the field-line denoted by path-num starting at tx ty with q1 as the source charge and charges (which may contain q1) as the participants in the field. The positions of the traced field line is encoded into the field.")) (defgeneric trace-field-lines (field q1 charges) (:documentation "Trace all field lines associated with the field starting from q1 with the participating charges. All information is stored into the field object."))

We'll take the methods that implement each generic function in turn.

Method SPARKS

This method computes the number of sparks this entity should emit when it explodes, dies, is removed from the game, etc.

(defmethod sparks ((ent entity)) (+ (initial-sparks ent) (random (additional-sparks ent))))

Method MARK-DEAD

This method marks the object as dead. At the end of each game step, the points from any dead entities are assigned to the player.

(defmethod mark-dead ((ent entity)) (setf (status ent) :dead))

Method DEADP

This method returns T if the entity is marked dead.

(defmethod deadp ((ent entity)) (eq (status ent) :dead))

Method MARK-STALE

This method marks the object as stale. At the end of each game step, any stale objects are removed. Stale objects do not participate in the collision detection algorithm.

(defmethod mark-stale ((ent entity)) (setf (status ent) :stale))

Method STALEP

This method returns T if the entity is marked stale.

(defmethod stalep ((ent entity)) (eq (status ent) :stale))

Method MARK-ALIVE

This method marks an entity as alive. I actually don't use it in the game since all entities default to being alive when created. No object, once not alive becomes alive again. However, I'm a sucker for symmetric APIs, so I include it.

(defmethod mark-alive ((ent entity)) (setf (status ent) :alive))

Method ALIVEP

This method returns T if the object is marked alive.

(defmethod alivep ((ent entity)) (eq (status ent) :alive))

Method DIE

When an entity dies we default to marking it dead and exploding it. However, when specifically an enemy does, there is a chance it could make a powerup or a mine.

;; If something is told to be dead, we kill it and blow it up. (defmethod die ((ent entity)) (mark-dead ent) (explode ent)) ;; When an enemy dies, there is a small chance a power up or a mine ;; gets created in the spot of death. (defmethod die :after ((ent enemy)) (let ((chance (random 1.0))) (cond ((< chance .25) (make-powerup ent)) ((and (>= chance .25) (< chance .50)) (make-mine ent)))))

Method DISTANCE

Return the Euclidean distance between two entities.

(defmethod distance ((a frame) (b frame)) (euclidean-distance (x a) (y a) (x b) (y b)))

Method ACTIVE-STEP-ONCE

The method active-step-once performs one active simulation step for an entity in the game world. It is called "active" because it simulates everything, such as physical movement or time passing, that aren't passive effects. Active effects must complete before any passive effects (like fields) can be computed. Passive effects depend heavily on a consistent spatio-temporal view of the world.

The most general active-step-once method moves the object along in the world and decrements the time to live, if any.

;; Perform one physical and temporal step in the simulation (defmethod active-step-once ((ent frame)) (incf (x ent) (dx ent)) (incf (y ent) (dy ent)) (unless (null (ttl ent)) (when (> (ttl ent) 0) (decf (ttl ent)))))

For player instances, we need to bound the movement of the ship to the game screen. This is rendered as an after-method to deal with the fact the frame may have moved beyond the edges of the screen. The bounding box specified is in terms of the world coordinates.

;; The player objects get bound to the edges of the screen (defmethod active-step-once :after ((ent player)) (with-accessors ((x x) (y y)) ent (when (< y .05) (setf y .05)) (when (> y .95) (setf y .95)) (when (< x .03) (setf x .03)) (when (> x .97) (setf x .97))))

If the ttl for any drawable hits zero, it goes stale. This easily implements the time limit on powerups and sparks.

;; When the ttl for any drawable hits zero, it goes stale and is ;; removed from the game world. (defmethod active-step-once :after ((ent drawable)) (unless (null (ttl ent)) (when (zerop (ttl ent)) (mark-stale ent))))

Shields get an active step, although Option-9 doesn't utilize this feature. It could be used for shields that have a time limit or other effect as such. It is here because sometimes I play with this feature in an experimental manner. If I continue to hack on this game and determine something fun and playable with it, I'll likely document it.

;; Some shield have internal behavior which needs simulating (defmethod active-step-once :after ((ent ship)) (when (ship-main-shield ent) (active-step-once (ship-main-shield ent)))) ;; Base shields don't do anything in their simulation step (defmethod active-step-once ((s shield)) nil)

And finally, Field Mines, which may have decided that they needed to follow a field line, should stop moving. If in the next frame, it decides to move again, fine.

;; Field mines only move once in their direction and then don't move again ;; unless they are being in collision with a field. If so, they are told ;; at that time to move again. (defmethod active-step-once :after ((ent field-mine)) (setf (dx ent) 0.0 (dy ent) 0.0))

Method PASSIVE-STEP-ONCE

In option-9, field generation is the only passive step which needs to be performed. When computing the field, all the active steps need to have been resolved. This is because you don't want to generate a field and then have half of the entites move after the field has been computed. That would be very physically incorrect. The generate method leads more into how the field itself gets created and we'll deal with that when we come to it.

;; Usually nothing has a passive step (defmethod passive-step-once (object) nil) ;; If the ship has a passive gun, then simulate it with the ship as ;; the source charge and other brains as the charges (defmethod passive-step-once :after ((ent ship)) (when (ship-passive-gun ent) (with-accessors ((players players) (enemy-mines enemy-mines) (enemies enemies) (enemy-shots enemy-shots)) (game-context ent) (let ((ents (loop for i in (list players enemy-mines enemies enemy-shots) appending i))) (generate (ship-passive-gun ent) ent ents)))))

Method RENDER

Rendering an entity means to look into the drawable and render the shape specified in the primitives slot at the location specified in the x and y slots in the frame. We again take advantage of careful specification of the entities in the asset file by choosing the exact names cl-opengl would have used for the primitives. This function should make it clear what is to be written in the :primitives list in the asset file--anything cl-opengl is willing to accept and can be specified by a vertex/color stream to the video card.

(defmethod render ((ent drawable) scale) (with-accessors ((x x) (y y) (dx dx) (dy dy)) ent (destructuring-bind (sx sy) scale ;; render a list of possibly differing primitives associated with this ;; shape (mapc #'(lambda (primitive) (gl:with-primitive (car primitive) ;; render each specific primitive (mapc #'(lambda (vertex/color) (destructuring-bind ((vx vy) (cx cy cz)) vertex/color (gl:color cx cy cz) (gl:vertex (+ x (* vx sx)) (+ y (* vy sy)) 0.0))) (cdr primitive)))) (primitives ent)))))

Rendering ships, however, need a little more work. Ship geometry is taken care of by the primary render method. Shields and any passive effects are rendered as an :after method for the ships. We also take opportunity to render a health bar (in reality just two fat line segments) above the ship in case it has been damaged.

This method explains why there is a make-instance-finish method for ships which convert the keyword of a unique shield entity as specified in the asset file into a real instance of the object. The slightly funny treatment of the shield origin of the shield is due to not having real reference frames into which objects can be embedded. In a more physically complex game, that's probably the first thing I'd extend the FRAME class to be...

;; Ships have various other things which need to be renderede as well. So ;; do them... (defmethod render :after ((s ship) scale) ;; If there is a passive-gun (which is field-like) render that. (when (ship-passive-gun s) (setf (x (ship-passive-gun s)) (x s) (y (ship-passive-gun s)) (y s)) (render (ship-passive-gun s) scale)) ;; If there is a shield, render that too. (when (ship-main-shield s) (setf (x (ship-main-shield s)) (x s) (y (ship-main-shield s)) (y s)) (render (ship-main-shield s) scale)) ;; If the hit-points is not equal to the maximum hit points, then ;; render the health bar (when (/= (hit-points s) (max-hit-points s)) (destructuring-bind (xscale yscale) scale (gl:line-width 4.0) (gl:with-primitive :lines (let* ((per (/ (hit-points s) (max-hit-points s))) (invper (- 1.0 per))) (gl:color 1 1 1) ;; start life bar (gl:vertex (+ (x s) (* -4 xscale)) (+ (y s) (* 5 yscale)) 0) ;; End life bar (gl:vertex (+ (x s) (* (- 4 (* 8 invper)) xscale)) (+ (y s) (* 5 yscale)) 0) ;; start filler (gl:color .2 .2 .2) (gl:vertex (+ (x s) (* (- 4 (* 8 invper)) xscale)) (+ (y s) (* 5 yscale)) 0) (gl:vertex (+ (x s) (* 4 xscale)) (+ (y s) (* 5 yscale)) 0))) (gl:line-width 1.0))))

Even though I haven't yet explained how the field paths are calculated, we have enough information to know how they can be drawn. The main loop of this render method is to iterate over the key/value pairs in the entity-contacts hash table in the field. For a particular PathContact, we dig out the FieldPath from the path array in the Field and render it as a line strip.

For any contacts which are actually touching a real entity, we pick a random bluish color for it. Any lines for the :no-collision set are just a dark blue. This gives an interesting visual effect. For contact points which hit enemies, we occasionally draw some sparks at the point of contact.

;; Draw the field lines before any actual geometry of the weapon. We ;; do this by walking the entity-contacts hash table which lets us ;; know how to render the various paths. We render this before the ;; regular render so any :primitives for the passive-gun get rendered ;; on top of the passive effect. (defmethod render :before ((f tesla-field) scale) (declare (ignorable scale)) (maphash #'(lambda (eid path-contact) (with-accessors ((pc-number-of-contacts number-of-contacts) (pc-path-ids path-ids)) path-contact (dolist (path-index pc-path-ids) ;; get the path which corresponds to the path-id at the ;; index (with-accessors ((fp-steps steps) (fp-path path)) (svref (paths f) path-index) ;; walk the path vector while drawing it (cond ((equal eid :no-collision) (gl:line-width 1.0) (gl:color .1 .1 .3)) (t (gl:line-width 2.0) (let ((r (random .3))) (gl:color (+ .1 r) (+ .1 r) (+ .2 (random .8)))))) (gl:with-primitive :line-strip (dotimes (vertex-index fp-steps) (let ((loc (svref fp-path vertex-index))) (gl:vertex (x loc) (y loc) 0)))) (gl:line-width 1.0) ;; If the contact point exists, draw it and emit some sparks ;; from it. (unless (or (equal eid :no-collision) (zerop fp-steps)) (let ((loc (svref fp-path (1- fp-steps)))) (when (zerop (random 10)) (emit-sparks 1 (x loc) (y loc) :ttl-max 5 :velocity-factor 1/20)))))))) (entity-contacts f)))

Method COLLIDE

The usually run primary method figures out if two objects collide in the game world. It is the first method using multiple dispatch, but it turns out not to be very interesting. When it calls the method perform-collide, that's when things get very interesting!

;; See if two collidables actually collide. (defmethod collide ((fist collidable) (face collidable)) (when (and (alivep fist) (alivep face)) (when (< (distance fist face) (max (radius fist) (radius face))) ;; tell both objects what they collided with. In practice this ;; means that by default, both will explode. (perform-collide fist face))))

If the ship has a passive gun, we see if it collided with the collidable.

;; Ships can have passive-guns, so here we compute if the passive gun ;; hit the other collidable. The face will hit back in this ;; context. :) (defmethod collide :before ((fist collidable) (face ship)) (when (ship-passive-gun face) (collide (ship-passive-gun face) fist)))

In this revision of Option-9, there is only one passive gun and that's a Tesla Field. Here we determine if the tesla-field contacts the collidable using the contacts method. If so, the perform-collide takes care of what to do about it.

;; If any field lines hit the face, perform the collision with it. (defmethod collide ((f tesla-field) (face collidable)) (when (contacts f face) (perform-collide f face)))

Method PERFORM-COLLIDE

The pieces are finally in place for the implementation of perform-collide. This method performs multiple dispatch to great effect. It figures out what to do for each (already known to have collided) entity based upon their type.

The most general perform-collide method is simply that two collidables damage each other. The details of the method damage will be shown shortly.

;; Primary method is both entities damage each other and one, both, or ;; neither may die. (defmethod perform-collide ((collider collidable) (collidee collidable)) (damage collider collidee))

When a collidable hits a ship, we first check to see if we have a shield and if the shield was willing to absorb the hit. If the shield can't or we don't have one, we fall towards the more general implementation of perform-collide which explodes both objects and marks them dead. Using multiple dispatch for the absorbs method greatly simplifies and makes robust this implementation. It prevents me from manually checking what kind of collidable hit what kind of shield. If the shield gets used up, we destroy it.

I think this is a good use of an :around method since it can totally block the continued generic dispatch of the perform-collide method in the event the shield absorbed the hit.

;; Here we handle the processing of a something hitting a ship which might ;; or might not have a shield. (defmethod perform-collide :around ((collider collidable) (collidee ship)) (if (ship-main-shield collidee) (multiple-value-bind (absorbedp shield-is-used-up) (absorbs collider (ship-main-shield collidee)) (if absorbedp (progn (die collider) (when shield-is-used-up (setf (ship-main-shield collidee) nil))) (call-next-method))) (call-next-method)))

When the player collides with a powerup, the player should get the benefits of the powerup. The comments imply that enemy ships could get a powerup. In fact I tested it (with some minor code changes) and they can--if I fix the collision sets, described later, which dictate what can collide with what in the game. However I didn't really implement particular shield geometries for each ship (though I did for enemy-3). The knowledge for choosing the right shield geometry for an arbitrary ship which picks it up is a problem I hadn't solved. Having this feature changes the game play significantly especially in terms of the AI. I just hadn't really thought about it beyond my minor testing.

;; The player gets the new weapon as denoted by the powerup, and the ;; powerup goes immediately stale. NOTE: If I change the collider type ;; to ship here, then ANY ship can get the powerup and its effects (as ;; long as I collide the enemies to the powerups in the main loop of ;; the game. However, I haven't coded the right geometries for the ;; ship shields or a good means to choose between them. So for now, ;; only the player can get powerups. (defmethod perform-collide ((collider player) (collidee powerup)) ;; A powerup can only be used ONCE (when (not (stalep collidee)) (mark-stale collidee) (when (powerup-main-gun collidee) (setf (ship-main-gun collider) (powerup-main-gun collidee))) (when (powerup-main-shield collidee) (setf (ship-main-shield collider) (make-entity (powerup-main-shield collidee)))) ;; If the powerup has a health level, apply it to the player. (incf (hit-points collider) (powerup-health-level collidee)) (when (> (hit-points collider) (max-hit-points collider)) (setf (hit-points collider) (max-hit-points collider))) ;; If I already have this weapon, then increase its power if possible. (when (powerup-passive-gun collidee) (if (ship-passive-gun collider) (increase-power (ship-passive-gun collider)) (setf (ship-passive-gun collider) (make-entity (powerup-passive-gun collidee)))))))

By default, when the Tesla Field is found to have contacted a collidable, it will probably damage it.

;; In general, the field damages the thing it touches. (defmethod perform-collide ((f tesla-field) (ent collidable)) (damage f ent))

However, Field Mines have a very special behavior. When a Tesla Field contacts a Field Mine, the Field Mine follows the field back to the source looking to destroy what it find there. It does this by summing the inverse direction vectors of the field paths as they come near the field mine. Then it sets its movement vector to be a little bit in that direction. There is a small "hack" in here in that we don't use the absolute last point of the field path to compute the paths direction. This is because the last step might be very near or past the origin of the Field Mine. In this case the direction at that point in the field might be not a part of that field line--due to the step error of the integration.

;; field mines follow the field back to the generating ship and are ;; NOT damaged by the field, therefore they are a new primary method. (defmethod perform-collide ((f tesla-field) (ent field-mine)) (let ((pc (contacts f ent)) (mx 0) (my 0)) ;; add up the inverse direction vectors from the participating ;; field paths. We're going to be following the field lines ;; back... (dolist (path-index (path-ids pc)) (with-accessors ((fp-steps steps) (fp-path path)) (svref (paths f) path-index) ;; Sum the inverted direction vectors found at the last field ;; path step. XXX It turns out when the last step was computed, it ;; is often past the object (since it was in the collision radius). ;; This messes up the computation of this vector so I use the second ;; to the last step, which should be ok in accordance to the algorithm ;; which produced the path. (when (> fp-steps 1) (let ((loc (svref fp-path (1- (1- fp-steps))))) (incf mx (* (dx loc) -1)) (incf my (* (dy loc) -1)))))) ;; Normalize the vector, set some fraction of it to be the ;; direction and speed the mine should move towards the ship. (multiple-value-bind (nvx nvy) (normalize-vector mx my) (setf (dx ent) (* nvx .003) (dy ent) (* nvy .003)))))

Method DAMAGE

By default, two collidables damage each other. If either of them falls below 0 hit points. It dies.

;; If two collidables damage each other, one or both can die. (defmethod damage ((ent-1 collidable) (ent-2 collidable)) (when (and (alivep ent-1) (alivep ent-2)) (decf (hit-points ent-1) (damage-points ent-2)) (when (<= (hit-points ent-1) 0) (die ent-1)) (decf (hit-points ent-2) (damage-points ent-1)) (when (<= (hit-points ent-2) 0) (die ent-2))))

If a hardnose shot hits a simple-shot and has more damage-points than hit points of the simple shot, it destroys it and keeps going. Otherwise, it lets the more general method take care of what happens.

;; If a hardnose-shot has equal or more hit points than the other simple-shot ;; it destroys it and keeps going undamaged. (defmethod damage :around ((ent-1 hardnose-shot) (ent-2 simple-shot)) (cond ((>= (damage-points ent-1) (hit-points ent-2)) (die ent-2)) (t ;; otherwise, we damage them both (call-next-method))))

A Super Shot is more powerful, it acts like a Hardnose Shot, except with all collidables instead of just simple-shots.

;; If a super-shot has equal or more hit points than the other collidable ;; it destroys it and keeps going undamaged. (defmethod damage :around ((ent-1 super-shot) (ent-2 collidable)) (cond ((>= (damage-points ent-1) (hit-points ent-2)) (die ent-2)) (t ;; otherwise, we damage them both... (call-next-method))))

Now we finally get to see what it means for a Tesla Field to damage a collidable that it is contacting. In this code, we take the ceiling of the number of contacts which are touching the entity divided by 10 and subtract it from the hit points of the collidable. If the hit points of the collidable falls below 0, it dies. Why 10, you ask? When I was playing the game, the tesla field was killing things pretty fast. So I just pulled a constant out of my butt until it played well. If I were to change the 60fps to something else, this would have to change as well. In writing game, one finds that there are just a lot of random constants shoved into places simply to make the game play well.

;; a tesla-field by its very nature will damage the thing it collided with (defmethod damage ((f tesla-field) (ent collidable)) (let ((path-contact (contacts f ent))) (decf (hit-points ent) (ceiling (number-of-contacts path-contact) 10)) (when (<= (hit-points ent) 0) (die ent))))

Method SHOOT

The player will fire straight up from its position. I'm also a jerk so each time you fire, you lose one point. Notice that the function modify-score needs the game context from the ship. This method also needs to know where to put the shot in the game context. As an aside, I could have just used a special variable everywhere which represented the game context, but I have an idea for this engine to be on a server somewhere which could be handling many games at once. In that case, this type of object dependency injection solution is better than a global variable.

;; The method for when the player ship shoots (defmethod shoot ((ship player)) (let ((shot (make-entity (ship-main-gun ship) :x (x ship) :y (+ (y ship) .03) :dx 0 :dy .022))) (push shot (player-shots (game-context ship)))) (modify-score (game-context ship) -1))

The enemy ships only fire straight down. Their shot velocity is the speed of the enemy in the y direction plus the speed of the shot and a small constant. This ensures the shot is always travelling faster than the enemy.

;; The method for when the enemy ship shoots. (defmethod shoot ((ship enemy)) (let ((shot (make-entity (ship-main-gun ship) :x (x ship) :y (- (y ship) .03) :dx 0 :dy (+ (- (+ .005 (random .005))) (dy ship))))) (push shot (enemy-shots (game-context ship)))))

Method ABSORBS

This small collection of methods sees if a shield can absorb a particular collider. If the collider is absorbed then the shield decrements how many more it can absorb. The returned values dictate if the absorption happened and if the shield can absorb more hits or not. This table dictates what happens when shots and ship collide with either the shot shield or the ship shield.

Shot Shield Ship Shield Shot absorbed absorbed Ship not absorbed absorbed

And the code which represents the above table:

;; By default, the shield will absorb the collider. (defmethod absorbs (collider (collidee shield)) (when (> (shots-absorbed collidee) 0) (decf (shots-absorbed collidee))) (values t (zerop (shots-absorbed collidee)))) ;; However, if a ship hits a shot-shield, the shield doesn't stop it ;; and the shield is destroyed. (defmethod absorbs ((collider ship) (collidee shot-shield)) (values nil t))

Method THINK

By default, nothing will do anything while thinking.

;; By default, nothing thinks... (defmethod think (ent) nil)

But, Enemies can think. This counts down until when they have an idea and afterwards sets up the time until they have their next idea.

;; but enemies think... (defmethod think ((ent enemy)) (when (until-next-action ent) (cond ((zerop (until-next-action ent)) (idea ent) (setf (until-next-action ent) (+ 15 (random 105)))) (t (decf (until-next-action ent))))))

Method IDEA

After all of that hoopla about AI and the need for it so different enemies can have different behavior, here is my stupid AI function. What an anti-climax eh? If there is a subsequent version of this game, I'll likely do something better here.

;; and enemies have ideas about what they want to do in the world... (defmethod idea ((ent enemy)) ;; Instead of doing anything cool like inspect the world, we'll just shoot (shoot ent))

Method CONTACTS

If a tesla field contacts an entity, return the path contact instance for it.

;; return the path-contact instance if the entity was contacted by any ;; field paths, otherwise nil. (defmethod contacts ((f tesla-field) (e entity)) (multiple-value-bind (path-contact presentp) (gethash (id e) (entity-contacts f)) (when presentp path-contact)))

Method GENERATE

The method generate is responsible for generating all passive effects. Since we only have one passive effect, the field, at this time, that's what this one does. Generation of a field means tracing the field paths from the source charge in relation to the rest of the charges. Keep in mind that the source charge is also in the charges list in order to perform the correct mathematics.

;; Actually generate the field and compute path contact information. (defmethod generate ((f field) source-charge charges) ;; Clear the contact hash because we're doing a new trace of the field. (clrhash (entity-contacts f)) (trace-field-lines f source-charge charges))

Method INCREASE-DENSITY

Passive effects, like fields, often have some kind of a density component, such as field strength at a spot, to them. In our case, this specifically means increasing the number of field paths traced from the source charge. We keep increasing it until the maximum is reached.

(defmethod increase-density ((tf tesla-field)) (unless (power-density-maxp tf) (incf (power-lines tf))))

Method INCREASE-POWER

When we get a Tesla Field powerup and increase the power of the weapon, we randomly determine if we're going to increase the density or the range. However, if we find that the one we chose is already at max, we try to increase the other one. This ensures that powerups aren't wasted.

;; We don't waste an increase of power if possible. (defmethod increase-power ((tf tesla-field)) (if (zerop (random 2)) (if (power-density-maxp tf) (increase-range tf) (increase-density tf)) (if (power-range-maxp tf) (increase-density tf) (increase-range tf))))

Method INCREASE-RANGE

Increase how far away from the ship the Tesla Field may wander.

(defmethod increase-range ((tf tesla-field)) (unless (power-range-maxp tf) (incf (power-range tf))))

Method POWER-DENSITY-MAXP

Return true if the density of the tesla-field is at maximum.

(defmethod power-density-maxp ((tf tesla-field)) (if (= (power-lines tf) 4) t nil))

Method POWER-RANGE-MAXP

Return true if the range of the tesla-field is at maximum.

(defmethod power-range-maxp ((tf tesla-field)) (if (= (power-range tf) 7) t nil))

Method TRACE-FIELD-LINE

Tracing a single field line is a bit of an interesting thing. The general algorithm is that we find the normalized direction of the field at the start of the path (given to us in tx ty), follow it by a small step away from the generator, do it again and again, and store each point along the integration path. During the trace, we determine if it hit anything or otherwise have to stop being traced.

Since I'm using such a crappy integration method, I noticed oscillations in the field lines when the step size was large enough to not suck too much CPU and the field line was near an asymptotic boundary in the field. To make this less apparent to ones eye, which is often looking at the player ship, nearby the ship I increase the resolution of the field integration. The actual equation is a quadratic falloff with some visually appealing constants. After a certain distance is reached, the integration becomes constantly spaced. This means that oscillations still can happen, but they'll be away from the ship and much less noticeable. I also put a cap on the number of steps a field line may be traced in order to handle saddle points or other oscillatory behavior in the field and due to the crappy integration method and/or large step size. As the path is being traced, each point along the path is stored for later display.

;; Starting at x,y trace a field line until we get close to a charge ;; which is not q1 or we go out of bounds. (defmethod trace-field-line ((f field) path-num tx ty q1 charges) (labels ((determine-field-line-direction (vx vy dx dy q) (multiple-value-bind (dx dy) (e-field-direction dx dy) ;; than vx vy is by itself, return -1, otherwise 1 (let ((nx (+ vx (* .001 dx))) (ny (+ vy (* .001 dy)))) (if (< (euclidean-distance (x q) (y q) nx ny) (euclidean-distance (x q) (y q) vx vy)) -1.0 1.0)))) (store-contact (f path-id entity-id xl yl) (multiple-value-bind (path-contact presentp) (gethash entity-id (entity-contacts f)) ;; Is there a better idiom for this? (unless presentp (let ((pc (make-pathcontact))) (setf (gethash entity-id (entity-contacts f)) pc) (setf path-contact pc))) (incf (number-of-contacts path-contact)) (push path-id (path-ids path-contact))))) (with-accessors ((fp-steps steps) (fp-path path)) (svref (paths f) path-num) (setf fp-steps 0) (let ((vx tx) (vy ty) (sum 0)) ;; First, we compute the field direction at the initial point, if ;; by following that vector, we get closer to q1, we'll reverse (when (or (< vx 0.0) (> vx 1.0) (< vy 0.0) (> vy 1.0)) (return-from trace-field-line)) ;; direction and follow the stream backwards. (multiple-value-bind (classification ex ey cent) (e-field charges vx vy) ;; XXX For traced paths which START in a colliding ;; situation, we ignore them. This might be bad and needs ;; revisiting. (when (eq classification :tracing) (multiple-value-bind (dx dy) (e-field-direction ex ey) (let ((dir (determine-field-line-direction vx vy dx dy q1))) ;; Now, we begin storing the line strip moving the ;; test charge from the start point to wherever it ends up ;; ensuring it goes in the right direction. ;; Keep walking the field line until we are done. We ;; bound it to (range f) to keep the oscillations from ;; the gross integration from overwhelming the ;; computation. (when (dotimes (index (range f) t) (when (or (< vx 0) (> vx 1) (< vy 0) (> vy 1)) ;; we went off the screen, so no contact. (return t)) (multiple-value-bind (classification ex ey cent) (e-field charges vx vy) ;; If we collide and the entity we collided with ;; is of an opposite charge, we're done. (when (and (eq classification :collision) (not (same-polarityp q1 cent))) (setf (x (svref fp-path index)) vx (y (svref fp-path index)) vy) (incf fp-steps) ;; Associate the contacting path-num with ;; the id of to whom it collided and where. (store-contact f path-num (id cent) vx vy) (return nil)) ;; otherwise we keep tracing the path (multiple-value-bind (dx dy) (e-field-direction ex ey) (setf (x (svref fp-path index)) vx (y (svref fp-path index)) vy (dx (svref fp-path index)) dx (dy (svref fp-path index)) dy) (incf fp-steps) ;; No direction guarantees that we loop ;; until done with no path movement, so we ;; bail. (when (and (= dx 0) (= dy 0)) ;; we died in a saddle point, no contact. (return t)) ;; Stepping to the next point is a little ;; interesting. We use a quadratic function ;; near the field generator to increase the ;; resolution of the field lines. As we go ;; farther away from the generator, we then ;; fixate the resolution to a ad hoc number ;; which looked good and was cheap to ;; compute for game play. This allows for us ;; to decrease the ugly oscillations right ;; nearby the ship, but not have to pay for ;; that oscillation reduction farther away ;; where oscillations are less likely to ;; happen. This equation to compute the ;; incremental was pulled out of my butt. (let* ((delta (/ (+ index 25) 450)) (incremental (if (< sum .15) (* delta delta) .015))) (incf sum incremental) (setf vx (+ vx (* dx incremental dir)) vy (+ vy (* dy incremental dir))))))) ;; If the dotimes returned true, it meant that the ;; path did not collide with any game entity but ;; instead ran out of range, got clipped, or ;; whatever. So it gets a special designation in the ;; entity-contacts table. (store-contact f path-num :no-collision vx vy))))))))))

Method TRACE-FIELD-LINES

Based upon the density of the field, represented in game terms by the number pf paths emanating from the source charge, we evenly distribute the field lines in a circle around the source charge. We trace each field line stepping slightly outside of the collision radius of the source charge

;; Starting at slightly more than radius from the object in world ;; space, trace each field-line whose number depends upon the charge. (defmethod trace-field-lines ((f field) q1 charges) ;; we're going to circle around the charge in even increments according ;; to num-paths (let* ((num-paths (num-paths f)) (delta (/ (* 2.0 pi) num-paths))) (flet ((start-point (x y path-num) (values (+ x (* (+ (radius q1) .001) (sin (* path-num delta)))) (+ y (* (+ (radius q1) .001) (cos (* path-num delta))))))) (dotimes (path-num num-paths) (multiple-value-bind (nx ny) (start-point (x q1) (y q1) path-num) (trace-field-line f path-num nx ny q1 charges))))))

Method SETF POWER-RANGE

This is a setf method for the power-range accessor in the Tesla Field. The purpose of this function is when the power-range changes, to update the internal slots and memory storage for the Field class. Specifically we translate the power-range, a number from 0 to 7, to a number of steps that must be walked in the field wrt the Field Class. Having this setf method simplifies a LOT of busy work for the Tesla Field when changing its range or density.

;; This will help us keep up to date numerous portions of the ;; telsa-field when the range for the tesla field changes. (defmethod (setf power-range) (range (tf tesla-field)) (assert (> range 0)) ;; The power range of the tesla-field can range from 1 to 7 (setf (slot-value tf '%power-range) (min range 7)) ;; Now we regenerate all of the internals. ;; First we figure out the real range of the traces, capped at 2^7 for ;; efficiency (otherwise we could waste 25% of our time in oscillations ;; or other integration foibles when computing the trace). (setf (range tf) (min (expt 2 (power-range tf)) (expt 2 7))) ;; Remake the paths array (setf (paths tf) (gen-paths (num-paths tf) (range tf))) ;; Clear out the contacts hash since it is now invalid (setf (entity-contacts tf) (make-hash-table :test #'equal)) range)

Method SETF POWER-LINES

This is a setf method for the power-lines accessor in the Tesla Field. The purpose of this function is when the power-lines changes, to update the internal slots and memory storage for the Field class. Specifically we translate the power-lines, a number from 0 to 4, to a number of field paths that must be walked in the field wrt the Field Class. Having this setf method simplifies a LOT of busy work for the Tesla Field when changing its range or density.

;; This will help us keep up to date numerous portions of the ;; telsa-field when the density (or number of path lines in the field ;; we will be tracing) for the tesla field changes. (defmethod (setf power-lines) (lines (tf tesla-field)) (assert (> lines 0)) (setf (slot-value tf '%power-lines) (min lines 4)) (setf (num-paths tf) (expt 2 (power-lines tf))) (setf (paths tf) (gen-paths (num-paths tf) (range tf))) (setf (entity-contacts tf) (make-hash-table :test #'equal)) lines)

The Electric Field Math

Ok, now that we've seen how we use and draw a Field and the Tesla Field, but how do we actually compute the field magnitude and direction at each point in the trace of the field path?

After consulting a physics book and thinking up ad hoc solutions to singularities in the math, I came up with the file field.lisp. The ad hoc solution was that I never let the field trace get close enough to the origin of a charge to explode and I check for zero divisors.

First, I need some vector math helpers:

(defun unit-vector (from tx ty) (multiple-value-bind (dist ndist) (euclidean-distance (x from) (y from) tx ty) (values (/ (- tx (x from)) dist) (/ (- ty (y from)) dist) ndist))) (defun normalize-vector (dx dy) (let ((dist (euclidean-distance 0 0 dx dy))) (if (= dist 0) (values 0 0) (values (/ dx dist) (/ dy dist)))))

And now comes the actual mathematics to compute the field. In this function, we compute the contribution of a single charge to the test point.

(defun e-field-one-point (from tx ty) (multiple-value-bind (ux uy ndist) (unit-vector from tx ty) (let ((c (/ (charge from) ndist))) (values (* c ux) (* c uy)))))

Since an electric field is a superposition of fields from all participating charges, we compute all contributions at the test point here using the electric field equation and coefficients. This function is interesting because if the test point is considered to be too close to another entity we mark it as a :collision, otherwise the test point is a :tracing point.

(defun e-field (charges tx ty) (let* ((const (/ 1.0 (* 4.0 pi 8.854187817e-12))) (nx 0) (ny 0)) (dolist (q charges) (if (< (euclidean-distance (x q) (y q) tx ty) (radius q)) (return-from e-field (values :collision 0 0 q)) (multiple-value-bind (qx qy) (e-field-one-point q tx ty) (incf nx qx) (incf ny qy)))) (values :tracing (* const nx) (* const ny) nil)))

It turns out that for the game effect, we don't actually care about the magnitude of the field at the text point, only its normalized direction.

(defun e-field-direction (dx dy) (normalize-vector dx dy))

To help us to determine if we have correct contact points, we can check to see if two charges are the same polarity or not.

;; Return true if c1 and c2 are both positive, or both negative, charges (defun same-polarityp (c1 c2) (or (and (> (charge c1) 0) (> (charge c2) 0)) (and (< (charge c1) 0) (< (charge c2) 0))))

And finally, we have a small pile of helper functions to initialize the internals of the Field instance. One thing to note here is that with each drawn frame we have to regenerate the field paths and all the contacts of those paths. That's a lot of information to regenerate and so we should be a little conservative in our memory usage to keep memory churn down. When the Field instance is created, we pre-allocate the storage for all of the field paths and how many possible steps each path could have taken.

;; Allocate the whole array which could hold up to 'steps' locations. (defun gen-path (steps) (make-array (list steps) :initial-contents (loop repeat steps collecting (make-instance 'location)))) ;; When we construct a fieldpath class, we initially set up all of the ;; memory we'll need to deal with it. Since we'll compute a fair ;; amount of information per frame, we don't want to generate too much ;; garbage. (defun make-fieldpath (max-steps) (make-instance 'fieldpath :path (gen-path max-steps))) (defun make-pathcontact () (make-instance 'pathcontact)) (defun gen-paths (num-paths steps) (make-array (list num-paths) :initial-contents (loop repeat num-paths collecting (make-fieldpath steps))))

The Game State

This code is found in game.lisp. It represents much of the interaction with the Game instance and some game logic about how to spawn things into the game world.

First thing's first the game instance constructor.

(defun make-game () (make-instance 'game))

With the loading of entities and the creation of the game state made possible, we make the obvious with-game-init macro. The binding of the entities database and game state to special variables allows us a hook later with the text console for the game.

(defmacro with-game-init ((filename) &body body) `(let ((*all-entities* (load-all-entities ,filename)) (*id* 0) (*game* (make-game))) ,@body))

Pausing the Game

Being able to pause to get screenshots or bring up the console is a good thing. In hindsight, it is suddenly obvious to me that this function is not lisp-like. It should really be something like (setf (paused g) (not (paused g))) instead.

(defun toggle-paused (g) (if (paused g) (setf (paused g) nil) (setf (paused g) t)))

Spawning & Moving the Player

YAY! We get to finally spawn a player instance! There will only be one player and so the players slot will be a list of one class instance of Player. Remember any arguments I specify to make-entity beyond the class name will override the :initform or anything specified in the asset file for that kind of object.

(defun spawn-player (game) (setf (players game) (list (make-entity :player :x .5 :y .05))))

A player has to be able to move around, right? It turns out that this isn't necessarily as easy as one would think. One option is to use a key press event and each time you get an event for a particular key you move the ship a little bit in that direction. However, that sucks because you only move as fast as the key repeat rate and you consume a LOT of I/O into the program handling all of those key presses. Another, better, option, is to use the key down and key up events for a key. When you get a key down event you move in a direction and keep moving that way for each simulation step. When you get a key up event, you stop moving in that direction.

The better movement option has a subtle problem. Just because you got a key up event doesn't mean you unconditionally write a zero to the player's dx or dy slot. An example is suppose you move LEFT, and while holding the LEFT key move RIGHT, then you release the LEFT key. If you are unconditionally affecting the dx dy slots, you'd stop moving all together, even though a key is down. Playing the game this way is amazingly frustrating and not fluid at all.

The way to lessen the pain and significantly improve game play is to only set the direction component to zero if the released key was the one for that direction. I've implemented this idea in move-player. I've also abstracted out the controller of the player. In move-player, it is called with :begin/:end state and a direction of :up/:down/:left/:right. Then internally it figures out if it should be moving in the direction or stopping. I used with-accessors here just to save some typing on my part.

(defun move-player (game state dir) ;; XXX this is for player one and it only assumes one player! (let ((p (car (players game)))) (with-accessors ((dx dx) (dy dy)) p (ecase state (:begin (ecase dir (:up (setf dy .015)) (:down (setf dy -.015)) (:left (setf dx -.015)) (:right (setf dx .015)))) (:end (ecase dir (:up (when (> dy 0) (setf dy 0))) (:down (when (< dy 0) (setf dy 0))) (:left (when (< dx 0) (setf dx 0))) (:right (when (> dx 0) (setf dx 0)))))))))

Spawning & Moving the Enemies

Since we can spawn a player, what about enemies? How do they get into the game state? This next function is serviceable, but not very maintainable. This is because I have to know the unique names of the enemies as defined in the asset file so I can write them into the bad-guys vector from which one is randomly chosen. If I continued writing on this game, I would likely add an :enemy-set list to the asset file which would contain that list so it would be data driven instead of manually edited. Better yet, I would have the load-all-entities code automatically dig through the entities looking for class symbol names which are subclasses of the ENEMY class and keeping track of them in another list. There are many options available to me if the game continued to evolve.

Enemies start at the top of the screen (y is near 1) and the ones on the left of the screen may move towards the right, and vice versa. Since dx and dy are set, the step-once simulation code takes care of their movement in the game world.

(defun spawn-enemy (game) (let ((bad-guys (vector :enemy-1 :enemy-2 :enemy-3)) (xloc (random 1.0))) (push (make-entity (svref bad-guys (random (length bad-guys))) :x xloc :y .95 :dx (* (random .001) (if (> xloc .5) -1 1)) :dy (- (random .01))) (enemies game))))

Rendering the Game World

We'd like to see the game state, right? This means we should render every drawable object in the game sets. Here we see another place that a hash table of collision sets could have been iterated over and everything inside of drawn instead of manually specifying it. But for us, this is good enough. The call to the render method will take care of knowing how to draw any particular object at the appropriate scale for the world.

(defun render-game (game scale) (with-accessors ((players players) (player-shots player-shots) (enemies enemies) (enemy-shots enemy-shots) (enemy-mines enemy-mines) (sparks sparks) (power-ups power-ups) (score-board score-board) (highscore-board highscore-board)) game (loop for i in (list score-board highscore-board players player-shots enemies enemy-shots enemy-mines sparks power-ups) do (loop for e in i do (render e scale)))))

Emitting Sparks

I have the same problem as the spawn-enemy function with the unique spark names from the asset file. Notice the db variable in the following code. Also, in this code I chose to use the *game* variable instead of passing in an entity. The reason is that sparks can be generated by all kinds of effects. Sometimes entities explode and sometime I get sparks from the fieldpath contacting an entity. In the former I would have access to the game world through the entity, but in the latter, there is no entity.

(defun random-sign () (if (zerop (random 2)) 1 -1)) (defun random-delta (&key (velocity .02)) (* (random velocity) (random-sign))) ;; For now, this needs the global game state to know where to emit the ;; sparks. This is because there may be times I want to emit sparks ;; into the World without having a known entity location. An example ;; is field path interactions with other entites. (defun emit-sparks (num-sparks x-loc y-loc &key (ttl-max 0 ttl-max-suppliedp) (velocity-factor 1)) (let ((db (vector :spark-1 :spark-2 :spark-3))) (dotimes (p num-sparks) (let ((spark (make-entity (svref db (random (length db))) :x x-loc :y y-loc :dx (* (random-delta) velocity-factor) :dy (* (random-delta) velocity-factor)))) (when ttl-max-suppliedp (setf (ttl-max spark) ttl-max)) (push (make-instance-finish spark) (sparks *game*))))))

Making Powerups

Here I make powerups and we continue to see the pattern of hardcoded values in a list to be randomly chosen. If I were to continue hacking on this game, I'd probably solve this (while adding the add-entity generic function) maintenance problem first thing.

;; Pick a random powerup to place in place of the enemy (defun make-powerup (ent) (let ((db (vector :powerup-hardnose :powerup-super-shot :powerup-shot-shield :powerup-ship-shield :powerup-tesla-gun :powerup-health))) (push (make-entity (svref db (random (length db))) :x (x ent) :y (y ent)) (power-ups (game-context ent)))))

Making Mines

So far there are only two types of mines in Option-9.

(defun make-mine (ent) (let ((db (vector :proximity-mine :field-mine))) (push (make-entity (svref db (random (length db))) :x (x ent) :y (y ent)) (enemy-mines (game-context ent)))))

Handling the Score Boards

About those score boards... The game context includes 4 slots grouped into 2 conceptual sets. The %score and %score-board slots are the integral score and the entities in a list which render that score into the game world. Same thing with %highscore and %highscore-board.

This function terribly computes the display boards for each score. I use hardcoded offsets for where the scores should start. Instead of doing the usual numerical algorithm to compute the digits in the score, I do it in an unorthodox manner with some convenient lisp functions. Likely this is a pretty expensive call to perform due to the method of digit extraction but it doesn't occur that often in the big scheme of things. I wouldn't know if it was important unless I profiled the game anyways. Seeing on how the game plays smoothly for me. I don't have to care.

(defun realize-score-boards (game) (let ((db `((#\0 . :digit-0) (#\1 . :digit-1) (#\2 . :digit-2) (#\3 . :digit-3) (#\4 . :digit-4) (#\5 . :digit-5) (#\6 . :digit-6) (#\7 . :digit-7) (#\8 . :digit-8) (#\9 . :digit-9))) (score-chars (reverse (map 'list #'identity (format nil "~D" (score game))))) (highscore-chars (reverse (map 'list #'identity (format nil "~D" (highscore game)))))) ;; realize the score board (setf (score-board game) nil) (let ((xstart .85) (xstep -.02) (ci 0)) (dolist (c score-chars) (push (make-entity (cdr (assoc c db)) :x (+ xstart (* xstep ci)) :y .98) (score-board game)) (incf ci))) ;; realize the highscore board (setf (highscore-board game) nil) (let ((xstart .15) (xstep -.02) (ci 0)) (dolist (c highscore-chars) (push (make-entity (cdr (assoc c db)) :x (+ xstart (* xstep ci)) :y .98) (highscore-board game)) (incf ci)))))

Here are some additional pieces of code to manipulate the score, highscore, and to ensure that the score display matches the integral number at all times.

(defun reset-score-to-zero (game) (setf (score game) 0) (realize-score-boards game)) (defun modify-score (game points) (incf (score game) points) (when (< (score game) 0) (setf (score game) 0)) (when (> (score game) (highscore game)) (incf (highscore game) (- (score game) (highscore game)))) (realize-score-boards game))

Simulating One Step in the Game World

Ah, this next idea is definitely the majority of the fruit from our labors. Here we perform one simulation step for the game world. This takes care of simulating each entity in the world, colliding everything in the different collision sets (and the order is important since you may collide with a shield powerup before a shot and that's advantageous to the player), removing dead or stale (which are out of bounds) entities, respawning the player after death, and spawning new enemies. The stepping of the game is split into distinct simulation phases in order to preserve consistency constraints. This begins to bring together the various pieces of the game into one unified whole.

(defun step-game (game) (with-accessors ((paused paused) (players players) (player-shots player-shots) (enemies enemies) (enemy-shots enemy-shots) (enemy-mines enemy-mines) (sparks sparks) (power-ups power-ups) (score score) (highscore highscore) (enemy-spawn-timer enemy-spawn-timer)) game (unless paused (let ((all-entities (list players player-shots enemies enemy-shots enemy-mines sparks power-ups)) ;; a small optimization which removes sparks from thinking ;; since there may be many of them and they aren't brains (brain-entities (list players player-shots enemies enemy-shots enemy-mines power-ups))) ;; 1. Simulate one active step for each entity. Specific methods ;; may do pretty complex things during their simulation step.... (loop for i in all-entities do (loop for e in i do (active-step-once e))) ;; 2. Simulate one passive step for each entity. Specific methods ;; may do pretty complex things during their simulation step.... ;; Since field calculations are passive and "always on", then this ;; information should be available to the entity during the think phase. (loop for i in all-entities do (loop for e in i do (passive-step-once e))) ;; 3. Each brain-entity can now think about what it wants to ;; do. It may shoot, change its direction vector, or do ;; something else. (loop for i in brain-entities do (loop for e in i do (think e))) ;; 4. Collide the various entity sets (flet ((collide-game-entity-sets (fist face) (dolist (left fist) (dolist (right face) ;; generic function does anything it wants (collide left right))))) (collide-game-entity-sets players power-ups) (collide-game-entity-sets player-shots enemy-mines) (collide-game-entity-sets player-shots enemy-shots) (collide-game-entity-sets player-shots enemies) (collide-game-entity-sets enemy-mines players) (collide-game-entity-sets enemy-shots players) (collide-game-entity-sets enemies players)) ;; 5. Remove any dead or stale entities, assigning points if necessary (flet ((remove-y/n (e) (when (deadp e) (modify-score game (points e))) (or (not (alivep e)) ;; remove if out of the displayed game world... (< (x e) -.05) (> (x e) 1.05) (< (y e) -.05) (> (y e) 1.05)))) (setf player-shots (remove-if #'remove-y/n player-shots) enemies (remove-if #'remove-y/n enemies) enemy-shots (remove-if #'remove-y/n enemy-shots) enemy-mines (remove-if #'remove-y/n enemy-mines) players (remove-if #'remove-y/n players) sparks (remove-if #'remove-y/n sparks) power-ups (remove-if #'remove-y/n power-ups))) ;; 6. If the player died, respawn a new one at the start location ;; and set the score back to zero. Bummer. (unless players (reset-score-to-zero game) (spawn-player game)) ;; 7. If enough time as passed, spawn an enemy (decf enemy-spawn-timer) (when (zerop enemy-spawn-timer) (setf enemy-spawn-timer (1+ (random 120))) (spawn-enemy game)) t))))

The Final Pieces

We finally come to the end of our road and have only two things left to implement from our original design. The text console to manipulate the game state and the main loop which runs everything. This code exists in the option-9.lisp file.

The Text Console

Many games (such as those using the quake engine) these days have a graphical console you can bring down into the game to adjust the game state. These are pretty cool, but a lot of work goes into them because you need the libraries to render the fonts, handle input, then an interpreter to process the input and mutate the game state. Unless a very large amount of time is spent on them, they are pretty damn limited and use a quirky domain specific language. What a pain.

This Is Lisp. Here we walk with giants.

Our text console is literally a text console as in it is not rendered with graphics. I did it this way only because I was lazy and didn't want to handle keyboard input or font management in the game. However, instead of a quirky little language you have to define with an interpreter for it, you have the full power of lisp to do whatever you want. To leave the console type (quit) or (exit) or (continue). Currently it relies on a terminal. I'll see about making it easier to build and run not only a stand alone app of this game, but also if you make it work in graphical lisp environments.

The first part of the text console are two functions provided by Pascal Bourguignon that implement a REPL that handles signaled conditions and provides the usual environmental bindings one expects. This replaces the naive REPL code I originally implemented which did none of these things.

;; Written by pjb with minor modification for finishing the output ;; from me. (defmacro handling-errors (&body body) `(handler-case (progn ,@body) (simple-condition (err) (format *error-output* "~&~A: ~%" (class-name (class-of err))) (apply (function format) *error-output* (simple-condition-format-control err) (simple-condition-format-arguments err)) (format *error-output* "~&") (finish-output)) (condition (err) (format *error-output* "~&~A: ~% ~S~%" (class-name (class-of err)) err) (finish-output)))) ;; Written by pjb with minor modification for finishing the output and ;; starting from zero in the history. (defun repl () (do ((+eof+ (gensym)) (hist 0 (1+ hist))) (nil) (format t "~%~A[~D]> " (package-name *package*) hist) (finish-output) (handling-errors (setf +++ ++ ++ + + - - (read *standard-input* nil +eof+)) (when (or (eq - +eof+) (member - '((quit) (exit) (continue)