summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dir-locals.el3
-rw-r--r--Makefile.am1
-rw-r--r--doc/Makefile.am1
-rw-r--r--doc/api/scripting.texi238
-rw-r--r--doc/sly.texi2
-rw-r--r--sly/actor.scm196
6 files changed, 440 insertions, 1 deletions
diff --git a/.dir-locals.el b/.dir-locals.el
index b4034ae..d7f9186 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -27,4 +27,5 @@
(eval . (put 'call-with-surface 'scheme-indent-function 1))
(eval . (put 'render/signal 'scheme-indent-function 1))
(eval . (put 'with-graphics 'scheme-indent-function 1))
- (eval . (put 'with-gl-bind-texture 'scheme-indent-function 2)))))
+ (eval . (put 'with-gl-bind-texture 'scheme-indent-function 2))
+ (eval . (put 'call-with-actor 'scheme-indent-function 1)))))
diff --git a/Makefile.am b/Makefile.am
index cfa1e4e..352a0d9 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -22,6 +22,7 @@ godir=$(prefix)/share/guile/site/2.0
SOURCES = \
sly/utils.scm \
+ sly/actor.scm \
sly/agenda.scm \
sly/audio.scm \
sly/config.scm \
diff --git a/doc/Makefile.am b/doc/Makefile.am
index 4f7d4a2..abf5170 100644
--- a/doc/Makefile.am
+++ b/doc/Makefile.am
@@ -9,6 +9,7 @@ sly_TEXINFOS = \
api/time.texi \
api/input.texi \
api/rendering.texi \
+ api/scripting.texi \
api/utils.texi
dvi: # Don't build dvi docs
diff --git a/doc/api/scripting.texi b/doc/api/scripting.texi
new file mode 100644
index 0000000..ca2cfd4
--- /dev/null
+++ b/doc/api/scripting.texi
@@ -0,0 +1,238 @@
+@node Scripting
+@section Scripting
+
+@menu
+* Overview:: Scripting basics.
+* Actors:: Scriptable game objects.
+* Actions:: Action combinators.
+@end menu
+
+@node Overview
+@subsection Overview
+
+@example
+(use-modules (sly actor))
+@end example
+
+An actor associates an object within the game world with a ``script''
+to control that object. For example, one can control a non-player
+character's movements in a role-playing game with an actor script.
+Or, a shoot-em-up game could make the game world itself an actor whose
+script adds enemies to the world at designated times. Like all
+high-level interfaces in Sly, it is purely functional.
+
+An actor is a wrapper around another game object. To create an actor,
+use the @code{make-actor} constructor:
+
+@example
+(make-actor (make-enemy) script)
+@end example
+
+Each script is composed of ``actions'', which are procedures that
+perform a user-defined transformation on a game object. Action
+procedures take three arguments: The ``world'', the ``effects'' list,
+and the game object itself.
+
+The world is an object that the action can query as part of its logic.
+For example, an enemy action that shot a bullet aimed at the player
+could query the world to find out the current location of the player.
+
+An effect is a procedure generated by an action that alters the game
+world rather than the actor itself. Going back to the previous
+example, in order for the enemy to actually shoot a bullet it would
+have to add to the effects list, because adding a bullet alters the
+world, not the actor.
+
+An action must return three values: the next action (or @code{#f} if
+there is nothing left to do), the new effects lists, and the new game
+object. Let's take a look at the simplest possible action,
+@code{idle}, which does not change the game object, nor add an effect,
+nor do anything else afterwards:
+
+@example
+(define (idle world effects object)
+ (values #f effects object))
+@end example
+
+Now for something a bit more complicated. Here's a higher-order
+procedure that returns an action that moves an enemy object
+horizontally:
+
+@example
+(define (move-horizontally x)
+ (lambda (world effects object)
+ (values #f effects (move-enemy object (vector2 x 0)))))
+
+;; Create a new action that moves the enemy by 3 units.
+(move-horizontally 3)
+@end example
+
+But what if one wants to wait a turn and then move horizontally?
+Enter action combinators, which allow multiple, simpler actions to be
+@emph{composed} into a complex action.
+
+To perform actions one after another, use the @code{sequence}
+combinator:
+
+@example
+(sequence idle
+ (move-horizontally 3)
+ idle
+ (move-horizontally 6))
+@end example
+
+To perform actions at the same time, use the @code{together}
+combinator:
+
+@example
+(together (move-horizontally 3)
+ shoot-at-player)
+@end example
+
+To perform an action in an unbounded loop, use the @code{forever}
+combinator:
+
+@example
+(forever (move-horizontally 3))
+@end example
+
+To perform an action a fixed number of times, use the @code{repeat}
+combinator:
+
+@example
+(repeat 10 (move-horizontally 3))
+@end example
+
+To perform one of two actions depending upon a condition, like
+@code{if} in regular Scheme code, use the @code{ifa} combinator:
+
+@example
+(ifa enemy-close-to-player?
+ shoot-at-player
+ (move-horizontally 3))
+@end example
+
+To take a single step in the actor script, use @code{update-actor}:
+
+@example
+(update-actor game-world effects-list actor)
+@end example
+
+The following sections provide a detailed reference for all of the
+procedures in the scripting interface.
+
+@node Actors
+@subsection Actors
+
+@deffn {Scheme Procedure} make-actor @var{object} @var{action}
+Create a new actor that wraps @var{object}, an arbitrary game object,
+and associates it with @var{action}, an action procedure.
+@end deffn
+
+@deffn {Scheme Procedure} actor? @var{object}
+Return @code{#t} if @var{object} is an actor.
+@end deffn
+
+@deffn {Scheme Procedure} actor-ref @var{actor}
+Return the object stored within @var{actor}.
+@end deffn
+
+@deffn {Scheme Procedure} actor-action @var{actor}
+Return the action for @var{actor}.
+@end deffn
+
+@deffn {Scheme Procedure} update-actor @var{world} @var{effects} @var{actor}
+Apply the action for @var{actor} with the given @var{world}, the game
+world object, and @var{effects}, the effects list.
+@end deffn
+
+@deffn {Scheme Procedure} actor-filter-update @var{predicate} @var{world} @var{actors}
+Update each actor in the list @var{actors} with respect to @var{world}
+and return a new list of actors whose objects satisfy @var{predicate}.
+@end deffn
+
+@deffn {Scheme Procedure} call-with-actor @var{actor} @var{proc}
+Apply @var{proc} with the object stored in @var{actor} and return a
+new actor containing the value returned from @var{proc}. The actor's
+action remains unchanged.
+@end deffn
+
+@node Actions
+@subsection Actions
+
+@deffn {Scheme Procedure} idle @var{world} @var{effects} @var{object}
+Do nothing. Do not change @var{object} nor add anything to
+@var{effects}.
+@end deffn
+
+@deffn {Scheme Procedure} wait @var{duration}
+Create an action that does nothing @var{duration} times.
+@end deffn
+
+@deffn {Scheme Procedure} forever @var{action}
+Create an action that performs @var{action} in an infinite loop, once
+per tick.
+@end deffn
+
+@deffn {Scheme Procedure} sequence @var{actions} @dots{}
+Create an action that sequentially performs each action in the order
+specified in the arguments list.
+@end deffn
+
+@deffn {Scheme Procedure} together @var{actions} @dots{}
+Create an action that concurrently performs each action in the order
+specified in the arguments list.
+@end deffn
+
+@deffn {Scheme Procedure} ifa @var{predicate} @var{consequent} @var{alternate}
+Create an action that performs @var{consequent} if @var{predicate} is
+satisfied, or @var{alternate} otherwise. @var{predicate} is a
+procedure that accepts a single argument: The game object stored
+within the actor that is performing the action.
+@end deffn
+
+@deffn {Scheme Procedure} whena @var{predicate} @var{consequent}
+Create an action that performs @var{consequent} when @var{predicate}
+is satisfied, otherwise nothing is done.
+@end deffn
+
+@deffn {Scheme Procedure} unlessa @var{predicate} @var{alternate}
+Create an action that performs @var{alternate} unless @var{predicate}
+is satisfied, otherwise nothing is done.
+@end deffn
+
+@deffn {Scheme Procedure} repeat @var{times} @var{action}
+Create an action that performs @var{action} @var{times} in a row.
+@end deffn
+
+@deffn {Scheme Procedure} action-lift
+Create an action constructor from @var{proc}, a procedure of any
+number of arguments whose first argument is the game object being
+transformed.
+
+@example
+(define (move-player player offset) ...)
+
+(define move-player* (action-lift move-player))
+
+;; Create a new action that moves the player horizontally.
+(move-player* (vector2 10 0))
+@end example
+
+@end deffn
+
+@deffn {Scheme Procedure} both @var{a} @var{b}
+Peform action @var{a} immediately followed by action @var{b}. When
+the action is run, the remainder of both @var{a} and @var{b} are
+returned as the next action to perform.
+
+This is a low-level combinator. Use @code{together} instead.
+@end deffn
+
+@deffn {Scheme Procedure} then @var{a} @var{b}
+Perform action @var{a} followed by action @var{b}. Unlike
+@code{both}, action @var{b} is not performed immediately after @var{a}
+finishes, but rather requires another tick.
+
+This is a low-level combinator. Use @code{sequence} instead.
+@end deffn
diff --git a/doc/sly.texi b/doc/sly.texi
index e0c081d..127f52b 100644
--- a/doc/sly.texi
+++ b/doc/sly.texi
@@ -167,6 +167,7 @@ See their respective home pages for installation and usage instructions.
* Booting:: Opening a window and running the game loop.
* Math:: Vectors, quaternions, matrices, etc.
* Time:: Tick-tock.
+* Scripting:: Functional game object scripting.
* Input:: Keyboard, mouse, and joystick input.
* Rendering:: Drawing to the screen.
* Utilities:: Miscellaneous conveniences.
@@ -175,6 +176,7 @@ See their respective home pages for installation and usage instructions.
@include api/init.texi
@include api/math.texi
@include api/time.texi
+@include api/scripting.texi
@include api/input.texi
@include api/rendering.texi
@include api/utils.texi
diff --git a/sly/actor.scm b/sly/actor.scm
new file mode 100644
index 0000000..fd21a64
--- /dev/null
+++ b/sly/actor.scm
@@ -0,0 +1,196 @@
+;;; Sly
+;;; Copyright © 2016 David Thompson <davet@gnu.org>
+;;;
+;;; Sly is free software: you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation, either version 3 of the License, or
+;;; (at your option) any later version.
+;;;
+;;; Sly is distributed in the hope that it will be useful, but WITHOUT
+;;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+;;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+;;; License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Purely functional game object scripting.
+;;
+;; Inspired by https://github.com/brandonbloom/bulletcombinators/
+;;
+;;; Code:
+
+(define-module (sly actor)
+ #:use-module (ice-9 match)
+ #:use-module (srfi srfi-1)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-11)
+ #:export (make-actor
+ actor?
+ actor-ref
+ actor-action
+ update-actor
+ actor-filter-update
+ call-with-actor
+
+ action-lift
+ idle
+ both
+ then
+ forever
+ repeat
+ wait
+ ifa
+ whena
+ sequence
+ together))
+
+
+;;;
+;;; Actors
+;;;
+
+(define-record-type <actor>
+ (make-actor object action)
+ actor?
+ (object actor-ref)
+ (action actor-action))
+
+(define (update-actor world effects actor)
+ "Apply the action for ACTOR with the given WORLD, the game world
+object, and EFFECTS, the effects list."
+ (match actor
+ (($ <actor> _ #f)
+ (values effects actor))
+ (($ <actor> object action)
+ (let-values (((new-action new-effects new-object)
+ (action world effects object)))
+ (values new-effects (make-actor new-object new-action))))))
+
+(define (actor-filter-update predicate world actors)
+ "Update each actor in the list ACTORS with respect to WORLD and
+return a new list of actors whose objects satisfy PREDICATE."
+ (let loop ((actors actors)
+ (effects '())
+ (results '()))
+ (match actors
+ (() (values (reverse effects)
+ (reverse results)))
+ ((actor . rest)
+ (let-values (((effects actor) (update-actor world effects actor)))
+ (if (predicate (actor-ref actor))
+ (loop rest effects (cons actor results))
+ (loop rest effects results)))))))
+
+(define (call-with-actor actor proc)
+ "Apply PROC with the object stored in ACTOR and return a new actor
+containing the value returned from PROC."
+ (let ((new (proc (actor-ref actor))))
+ (make-actor new (actor-action actor))))
+
+
+;;;
+;;; Actions
+;;;
+
+(define (action-lift proc)
+ "Create an action constructor from PROC, a procedure of any number
+of arguments whose first argument is the game object being
+transformed."
+ (lambda args
+ (lambda (world effects object)
+ (values #f
+ effects
+ (apply proc object args)))))
+
+(define (idle world effects object)
+ "Do nothing. Do not change OBJECT nor add anything to EFFECTS."
+ (values #f effects object))
+
+(define (both a b)
+ "Peform action A immediately followed by action B. When the action
+is run, the remainder of both A and B are returned as the next action
+to perform."
+ (lambda (world effects object)
+ (let-values (((next new-effects new-object)
+ (a world effects object)))
+ (if next
+ (let-values (((next* new-effects* new-object*)
+ (b world new-effects new-object)))
+ (if next*
+ (values (both next next*) new-effects* new-object*)
+ (values next new-effects new-object*)))
+ (b world new-effects new-object)))))
+
+(define (then a b)
+ "Perform action A followed by action B. Unlike 'both', action B is
+not performed immediately after A finishes, but rather requires
+another tick."
+ (lambda (world effects object)
+ (let-values (((next new-effects new-object)
+ (a world effects object)))
+ (values (if next (then next b) b)
+ new-effects
+ new-object))))
+
+(define (forever action)
+ "Perform ACTION in an infinite loop."
+ (define (forever world effects object)
+ (let-values (((next new-effects new-object)
+ (action world effects object)))
+ (values (if next (then next forever) forever) ; memoize?
+ new-effects
+ new-object)))
+ forever)
+
+(define (repeat times action)
+ "Perform ACTION TIMES times in a row."
+ (cond ((zero? times) idle)
+ ((= times 1) action)
+ (else (then action (repeat (1- times) action)))))
+
+(define (wait duration)
+ "Do nothing DURATION times."
+ (repeat duration idle))
+
+(define (ifa predicate consequent alternate)
+ "Create an action that performs CONSEQUENT if PREDICATE is
+satisfied, or ALTERNATE otherwise. PREDICATE is a procedure that
+accepts a single argument: The game object stored within the actor
+that is performing the action."
+ (lambda (world effects object)
+ (let ((action (if (predicate object) consequent alternate)))
+ (action world effects object))))
+
+(define (whena predicate consequent)
+ "Create an action that performs CONSEQUENT when PREDICATE is
+satisfied, otherwise nothing is done."
+ (ifa predicate consequent idle))
+
+(define (unlessa predicate alternate)
+ "Create an action that performs ALTERNATE unless PREDICATE is
+satisfied, otherwise nothing is done."
+ (ifa predicate idle alternate))
+
+(define (sequence . actions)
+ "Create an action that sequentially performs each action in
+ACTIONS."
+ (let loop ((actions actions))
+ (match actions
+ (() idle)
+ ((action) action)
+ ((action . rest)
+ (then action (loop rest))))))
+
+(define (together . actions)
+ "Create an action that concurrently performs each action in
+ACTIONS."
+ (let loop ((actions actions))
+ (match actions
+ (() idle)
+ ((action) action)
+ ((action . rest)
+ (both action (loop rest))))))