Skip to content

Latest commit

 

History

History
383 lines (328 loc) · 18 KB

DRAFT-HACKING.org

File metadata and controls

383 lines (328 loc) · 18 KB

WORK IN PROGRESS

This document is a work in progress. Please do not take it seriously.

Units

Distance

Distance units are measured in pixels.

Note that these pixels may be scaled up or down when rendered to fit on various screens.

Time

Unless otherwise stated in the variable name, all time units are assumed to be milliseconds.

*start-time* ; assumed to be milliseconds
*start-time-ms* ; explicitly milliseconds
*start-time-seconds* ; explicitly seconds

Wallclock time

TICKS and TICKS-NANOS return a millisecond and nanosecond timestamp respectively. This timestamp begins counting from an arbitrary point in time.

(ticks) ; -> ms timestamp
(ticks-nanos) ; -> nanosecond timestamp

Scene Time / Game Time

The amount of wallclock time passed will often not equal the amount of “game time” elapsed. For example, if the game is paused while an attack is charging, the amount of time paused must not affect the game-object’s charge time.

Each scene has a SCENE-TICKS function, which returns the amount of milliseconds of update time elapsed since the scene began updating.

You will almost always want to use SCENE-TICKS to implement in-game timers.

;; *scene* begins running
(scene-ticks *scene*) ; -> 0
;; 4 seconds of game time have elapsed
(scene-ticks *scene*) ; -> 4000
;; game is paused for 5 minutes, then unpaused
;; *scene* internal timer is the same because it was not recieveing updates while paused.
(scene-ticks *scene*) ; -> 4000

Core Components

Vert provides a series of core components used to create a game. Several of these components may be extended to allow

Events

Vert’s event api allows for objects or systems to broadcast events globally.

Interested parties may hook and respond to events in a number of ways.

Defining and Publishing Events

The defevent macro defines an event and an optional body of code to run when the publisher fires an event.

(defevent game-window-resized (game-window old-width old-height new-width new-height)
  "Docstring goes here"
  (log:debug "Optional body of code to run. goes here"))

;; now any lisp object can publish that event
(event-publish game-window-resized *engine-manager* 16 9 32 18)
;; of course, the player publishing window resize events make no sense. Please use responsibly.
(event-publish game-window-resized *player* 16 9 32 18)

Reacting to events

Subscribing to Events from a specific subscriber instance

It’s often the case that one instance of a class wishes to subscribe to events from another instance.

Rather than requiring subscribers to manually scan the event bus, DEFEVENT-HANDLER and EVENT-SUBSCRIBE provide a convenient way to hook these events.

(defevent-handler agent-killed ((pub agent) (sub scene))
  ;; handler will run for all SUBs which have EVENT-SUBSCRIBE'd to PUBs
  (log:debug "Agent killed (~A). Removing from scene ~A" pub sub)
  (remove-from-scene sub pub))

;; Simply defining a handler does nothing. A specific sub instance must subscribe to a specific pub instance
(event-subscribe agent-killed *agent1* *scene*)
(event-subscribe agent-killed *agent2* *scene*)
;; now *scene* can run a hook when either *agent1* or *agent2* is killed

Global callbacks to events

In addition to specific subscriber instances, the event system allows for global subscribers to hook all types of an event.

For example, define a handler to run every time the window is resized.

(defevent-handler-global log-game-window-resized (game-window-resized (window system-window) old-w old-h new-w new-h)
  (log:info "Game window resized! ~Ax~A" new-w new-h))

Manually Scanning the Event Bus

At the lower level, it’s possible to manually scan the event bus. This can be a costly operation, so it’s best to use the higher level event handlers.

The do-events macro will iterate every event published in the previous update frame.

(do-events (event-name event-publisher event-args)
  (when (equal event-name DESIRED-EVENT)
    'dispatch-on-event
    (return)))

Ordering of pub/sub body invokes

Event bodies are guaranteed to run before any subscriber callbacks. There is no guarantee on the order in which sub callbacks are run.

Systems

Systems are globals which provide access to shared resources.

Config

Interesting Globals

Graphics

Graphics use OpenGL 3.3.1. The window and gl-context are created by sdl2 (see sdl-engine-manager for context creation).

Rendering Overview

A lisp wrapper over the opengl context is stored in the global *GL-CONTEXT*.

Rendering is implemented with the RENDER method for the component you wish to render. Within the render method’s body, modify the gl-context to make the changes required to render your component.

(defclass my-component (game-object)
  ())

(defmethod render ((object my-component))
  ;; modifications to the gl context go here
  )

RENDER will be called for every visible object once per render frame.

Effects with gl-pipeline

Often it is desired to add rendering effects to a component (glow, explosion, etc). Vert provides the gl-pipeline utility to combine certain opengl operations.

(defclass my-component (game-object gl-pipeline)
  ())

(defmethod initialize-instance :after ((my-component my-component) &rest args)
  (declare (ignore args))
  ;; first step of the pipeline will be to render the gl texture with id 78
  (gl-pipeline-add-effect my-component
                          (make-instance 'gl-texture-quad
                                         :texture-id 78))
  ;; now add an effect which can show electrical shocks
  (gl-pipeline-add-effect my-component
                          (make-instance 'electricity-effect))
  ;; set up remaining effects here, or dynamically if you wish.
  )

Base Rendering Classes

You probably don’t need to implement rendering yourself. Instead consider using or extending the built-in rendering components.

All of these utils are subclasses of gl-pipeline

  • static-sprite : render a sprite, or a portion of a sprite.
  • animated-sprite : render a sequence of component-defined static-sprites to create an animation.
  • font-drawable : render text

Post-Process Effects

To modify an entire scene a post-process effect may be added to the scene.

Each post-process effect defines an input and output FBO. The scene will render into the input-texture and pass the output-fbo to the next effect’s input-texture. When it reaches the final effect the output will be sent to opengl’s final output buffer.

As with game-object components, each post-process-effect is implemented by specializing the RENDER method.

(defclass blur-effect (post-process-effect) ())

;;
(gl-pipeline-add-post-process-effect *scene*
                               (make-instance 'blur-effect
                                              ;; numbers are gl ids
                                              :input-texture 1
                                              :output-fbo 2))

(defmethod render ((effect blur-effect))
  ;; modifications to the gl context go here
  )

Other Rendering Utils

instance-renderer
render-queue
OpengGL Utils

Vert provides a series of opengl utils to implement rendering or add a new rendering effect.

  • The gl-utils file contains classes to represent opengl objects
    • gl-context
    • shader
    • texture
    • non-consing cl-opengl functions.

gl-drawable

  • effects-pipeline
    • pre-render
    • post-render

scene

  • effects-pipeline ; runs on entire image

Audio

Caches

Resource Autoloader

Everything else

Game-Object

Vert Primitives

  • Event
  • Systems
  • Game-Object
  • Scene

Game-Object

GAME-OBJECT is one of the foundational elements of vert.

A game object has the following

  • Position (x y z) and dimensions (width height).
  • color ()

The base game-object provides default implementations of these methods. Various game-components can (and will) override these default implementations to provide the interesting game logic.

In Vert, the distinction between a game-object and game-component is entirely a matter of convention. Game Components and Game Objects are both extensions of game-object.

Extending Game-Object to create custom game-objects and components

Game-Object presents a clos-like api for slot access, function invocation, and inheritance. The game-object api is currently implemented with clos, but this may change over time. Game programmers should not directly extend game-object or specialize its methods.

Instead, a series of macros are provided to define custom game-object behavior. These macros allow the implementation of Vert to change over time and reduce clos boilerplate code that would otherwise be required.

Extending Game-Object example

Let’s say you wanted to create a new “landmine” game-object. For simplicity, we’ll have our landmine check its proximity every update frame and log a message if it finds anything.

Game-Object Macros:

  • extension and creation
    • defgame-object
    • make-game-object-instance
  • slot access
    • game-object-slot
    • with-game-object-slots
    • with-game-object-accessors
  • invoking game object functions
    • game-object-call-next
    • game-object-funcall
(defgame-object landmine (game-object) ; parent game-objects/components
  (:documentation "Log a message if another object is in its proximity")
  ;; slots are created like clos slots. Accessed with `game-object-slot`
  (:slots (radius :documentation "proximity radius (from center) the landmine will check" :initform 4.0))
  (:init (landmine) ; constructor
         (log:info "Landmine created: ~A" landmine))
  (:update (landmine)
           (prog1 (game-object-call-next-method landmine) ; call and return parent method
             ;; NOTE: For simplicity, we'll iterate all objects in the scene. This is not best practice.
             (do-spatial-partition (neighbor (spatial-partition *scene*))
               (unless (eq landmine neighbor)
                 (when (< (distance-between neighbor landmine)
                          (game-object-slot-value landmine 'radius))
                   (log:info "BOOM! ~A <> ~A" landmine neighbor)))))))
;;; now create and use it
(add-to-scene *scene*
              (make-game-object-instance 'landmine))

You may have noticed that defgame-object takes a series of :keyword arguments which define various behaviors. It’s possible for a game-object to define its own keyword initializer as well.

Going back to our landmine example, it may be vary common for different game objects to know if there’s another object near them. For example, maybe a powerup would like to glow when the player is nearby.

To avoid duplicating the “is there something near me?” logic of landmine, we’ll extract the logic to a shared proximity game component. This will allow us to simplify our landmine code to this:

(defgame-object proximity (game-object)
  (:documentation "utility component to allow objects to respond to other, nearby objects.")
  (:object-init-symbols
     ;; can be a keyword or just a regular symbol
   (on-proximity :function (proximity neighbor distance-between)
                 (log:info "Optional, default proximity fn defined here.")))
  (:update (proximity)
           (prog1 (game-object-call-next-method proximity)
             ;; NOTE: For simplicity, we'll iterate all objects in the scene. This is not best practice.
             (do-spatial-partition (neighbor (spatial-partition *scene*))
               (unless (eq proximity neighbor)
                 ;; use GAME-OBJECT-FUNCALL to invoke the function we definied with :OBJECT-INIT-KEYWORDS
                 (game-object-funcall on-proximity proximity neighbor (distance-between proximity neighbor)))))))

;; now it's trivial for subclasses to define proximity logic

(defgame-object landmine (proximity) ; we'll simply extend proximity
  (:documentation "Log a message if another object is in its proximity")
  (:slots (radius :documentation "proximity radius (from center) the landmine will check" :initform 4.0))
  (:init (landmine) ; constructor
         (log:info "Landmine created: ~A" landmine))
  ;; now we can use its :proximity keyword when we define our landmine game-object
  (on-proximity (landmine neighbor distance-between)
                (when (< distance-between (game-object-slot-value landmine 'radius))
                  (log:info "BOOM! ~A <> ~A" landmine neighbor))))

Components which load external resources

Scenario: your game component requires external resources (CFFI array, opengl bits, sfx bits).

Your component must:

  1. Not attempt to load these bits when initialized. You should be able to create your component without a game window, gl-context, audio buffer, etc.
  2. When the engine starts, load the appropriate resources
  3. When the engine stops, release the appropriate resources
  4. When the component is dereferenced, release the appropriate resources before the engine shuts down

Recommended Approach

How resources are managed is ultimately up to the component developer, but it is highly recommended to do the following:

  1. Hook LOAD-RESOURCES and RELEASE-RESOURCES for your component (either use an :AROUND, :AFTER, or simpley CALL-NEXT-METHOD)
  2. When the object is initialized, register it with the RESOURCE-AUTOLOADER
  3. When the object’s resources are loaded, use the RESOURCE-RELEASER util to add a finalizer to the object’s resources if it is dereferenced
  4. When the object’s resources are released, cancel the resource releaser

As an example, we’ll consider a bomb component. This is a contrived example for educational purposes. In practice the rendering and audio logic would be broken out into simpler utility components which manage the underlying bits.

(defclass bomb (game-object)
  ((releaser :initform nil)
   (spritesheet :initform nil)
   (explode-sfx :initform nil)))))

   ;; Note: Hooking :AROUND so that all initializations are complete before resource-autoloader potentially call LOAD-RESOURCES
(defmethod initialize-instance :around ((bomb bomb) &rest args)
  (declare (optimize (speed 3)))
  (let ((all-args (append (list bomb) args)))
    (prog1 (apply #'call-next-method all-args)
      (resource-autoloader-add-object *resource-autoloader*
                                      (tg:make-weak-pointer bomb)))))

(defun %release-bomb-resources (spritesheet explode-sfx)
  (release-spritesheet spritesheet)
  (release-sfx explode-sfx))

(defmethod load-resources ((bomb bomb))
  ;; first make sure parent loading works
  (prog1 (call-next-method bomb)
    (unless (slot-value bomb 'releaser)
      (let ((spritesheet (make-spritesheet *gl-context* (resource-path "./art/bomb.png")))
            (explode-sfx (make-sfx *audio* (resource-path "./sfx/explode.wav"))))
        (setf (slot-value bomb 'spritesheet) spritesheet
              (slot-value bomb 'explode-sfx) explode-sfx
              (slot-value bomb 'releaser)
              ;; Note that passing BOMB in the first arg will NOT create a hard ref.
              ;; Whatever is passed there is convereted to a string for logging purposes. No hard refs will be created.
              ;; Using BOMB in the body, on the other hand, WILL create a hard ref and must not be done.
              (make-resource-releaser (bomb)
                (%release-bomb-resources spritesheet explode-sfx)))))))

(defmethod release-resources ((bomb bomb))
  (with-slots (releaser spritesheet explode-sfx) bomb
    (prog1 (call-next-method bomb)
      (when releaser
        (%release-bomb-resources spritesheet explode-sfx)
        (cancel-resource-releaser releaser)
        (setf releaser nil
              spritesheet nil
              explode-sfx nil)))))

Utility Components

Vert exposes utility components for common game operations.

Game devs aren’t required to use any of these, but it’s very likely a dev will want to use or extend many of these utils.

  • Transform
  • 2D Physics
  • Sprite Rendering
  • Font Rendering
  • Instanced Sprite Rendering
  • State Machine util

Scene

A scene is something which can be rendered and updated, just like a game-object. The main game loop maintains one scene instance, stored in SCENE global. The game loop will update and render this scene to keep a fixed timestep.

The CHANGE-SCENE fn will change the game loop’s active scene.

GAME-SCENE

A game-scene holds a collection of GAME-OBJECTs, calls UPDATE and RENDER, and provides an api to access objects in the scene in an efficient manner.

Menu

Renders a menu. A tree of text nodes with one active node at a time, which may be selected.

Leaf nodes run user-defined actions when selected.

Pause Scene

A scene which holds another scene. This other scene is rendered after the pause-scene, but not updated.

Overlays

Overlays are objects which are rendered in the scene in a camera independent manner. Used to implement HUDs.