diff options
author | David Thompson <dthompson2@worcester.edu> | 2016-04-18 20:39:39 -0400 |
---|---|---|
committer | David Thompson <dthompson2@worcester.edu> | 2016-04-18 20:39:39 -0400 |
commit | 9972e5c26da25c29939e5a75f59db5d58c14a31c (patch) | |
tree | 498b4a906b7afe4105b3677d2e5f5c7fa075c7cd | |
parent | 049964583b191f2302ff666f97ca23417b80762e (diff) |
Add actor module.
* sly/actor.scm: New file.
* Makefile.am (SOURCES): Add it.
* doc/api/scripting.texi: New file.
* doc/Makefile.am (sly_TEXINFOS): Add it.
* doc/sly.texi: Include scripting section.
* .dir-locals.el: Add identing rule for call-with-actor.
-rw-r--r-- | .dir-locals.el | 3 | ||||
-rw-r--r-- | Makefile.am | 1 | ||||
-rw-r--r-- | doc/Makefile.am | 1 | ||||
-rw-r--r-- | doc/api/scripting.texi | 238 | ||||
-rw-r--r-- | doc/sly.texi | 2 | ||||
-rw-r--r-- | sly/actor.scm | 196 |
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)))))) |