Jump to content

  • Log In with Google      Sign In   
  • Create Account

ApochPiQ

Member Since 17 Jul 2002
Offline Last Active Today, 07:51 PM

Topics I've Started

Request for Feedback: FormulaEngine game scripting system

17 January 2016 - 04:42 PM

So for a variety of reasons I've found myself looking into unusual or even novel models of scripting for implementing gameplay logic. Most models seem to draw heavily on imperative programming with a little bit of extra sauce of various flavors, depending on the framework.

 

FormulaEngine is a homegrown approach to creating gameplay logic. The elements are simple and yet highly composable, meaning it is easy to build complex logic systems that do pretty much anything. My goal is to minimize the amount of bindings needed between the scripting core itself and the C++ side of the game engine, something that I think I've accomplished at this point.

 

You can see the complete project on GitHub along with a simple "MUD engine" that supports rooms and items. I'm continually working on this so expect more features to pop up in the coming weeks.

 

 

I'm interested in feedback on this system on a number of facets:

  • Does the approach seem sensible? Do you foresee awkward limitations or bottlenecks in the architecture?

  • How intuitive is it to extend the logic of FormulaMUD?

  • What sorts of improvements would you make to the system to make it appealing for actual use?

  • Is the approach overall something you think you might experiment with?

 

 

Thanks in advance.


Proposal: "Tasks" in the Epoch programming language

13 December 2015 - 10:53 PM

For the past several years I've been working on designing and implementing the Epoch programming language.
 
The existing core of the language is fairly solid at this point, with most of my work (when it happens) going into cleaning up and refining the implementation of the ahead-of-time compiler and the associated IDE and toolchain. My other primary time sink, however, involves designing out parts of the language that are not yet specified. I'm trying to keep to the core structure and feel of the language as much as possible while adding functionality primarily in the form of well-separated features that can compose in rich ways.
 
One thing the language is weak at right now is encapsulation and separation of concerns. It feels very much like a C or Pascal era language, with few abstractions for grouping related data and code. I want to move past this into a future where composing fundamental language features allows for very powerful control over how programs are structured and how interrelated systems connect to each other.
 
 
My solution to this is a notion I've started referring to as tasks. In principle a task should solve many of the same problems that objects solve in other languages. Moreover, tasks should seamlessly integrate with a "green thread" model that I want for the runtime at some point in the future.
 
There are many considerations that have gone into the current design:
  • Creation and management syntax
  • Binding instances of a task to names
  • Construction into a valid state by default, i.e. no need for two-phase initialization
  • Provide encapsulation and composition tools
  • Elegant handling of internal, hidden state
  • Minimal extra syntax required
  • Need a way to truly hide API/state surface from consumers
Fundamentally, the resulting solution is pretty simple. (Fans of PLT will note that this is really just a hefty dose of the object-closure isomorphism.)
 
There are three components:
  • A function which has an internal block called a "dispatcher"
  • Messages which are received and handled by the dispatcher
  • Syntax for invoking the function and creating a task that can be messaged
In other words, a task is a closure that can be sent messages to alter its internal state, or retrieve computed values. Any function with an embedded dispatch {} block can be invoked to create a task. Tasks may be stored either on the stack or the free-store at the programmer's discretion; this permits the use of custom allocators.
 
All well and good. What does it look like?
 
Averager : -> task
{
    integer total = 0
    integer count = 0
 
    dispatch
    {
        DataPoint : integer x { total += x   count++ }
        GetAverage : -> total / count
    }
}
 
entrypoint :
{
    task avg = Averager()
    avg => DataPoint(42)
    avg => DataPoint(666)
 
    print(avg => GetAverage())
}
Note that Averager looks like a function of no parameters and a special return signature of "task."

Internally, it behaves like a function at first: it creates two local variables and initializes them. Next, we encounter the dispatch {} block. This block sets up a message handler structure that is bound to the return value of the function. When the function returns, its local variables are stored in a closure and the dispatch table is kept alongside them, much like a v-table in other languages.

The entrypoint function first invokes Averager() to create a task, and then stashes the closure in the variable avg. Next, it sends two DataPoint messages with some values to the closure. These are handled by the correspondingly-named pattern matchers in the dispatch block inside Averager(). Last but not least, avg is sent the GetAverage() message (which is really just a function call bound to the closure avg) and the result is printed to the screen.



This is of course a very simplistic example. A more interesting example involves polymorphism. The way to achieve this in Epoch is to use protocols. If a task implements all the messages specified by a protocol, it is said to be compatible with that protocol.

protocol Average :
    (DataPoint : integer),
    (GetAverage : -> integer)

// This would realistically use an enumeration instead of magic values
MakeAverager : 0 -> Average avg = AverageMean()
MakeAverager : 1 -> Average avg = AverageMedian()
MakeAverager : 2 -> Average avg = AverageMode()
MakeAverager : integer invalid -> Average avg = AverageMean() { assert(false) }

entrypoint :
{
    Average avg = MakeAverager(random(0, 3))

    avg => DataPoint(42)
    avg => DataPoint(666)

    print(avg => GetAverage())
}
Note that the syntax for a protocol is much like a structure that contains only function pointers.

We use pattern matching here in MakeAverager to select a particular sort of average based on a numerical input. The return type is a protocol, meaning that the function is free to return any task that is compatible with the named protocol, in this case Average.

The entrypoint works much the same as before, except this time it indirectly creates a task compatible with Average using a random number.


So ultimately, the syntax is very simple. "task" is a special type placeholder with limited application, akin to var or auto, designed to help enforce the contracts of the type system without requiring the programmer to utter really gross type signatures - or, worse, redundant information the compiler already knows.

Binding a task to a name looks like any other variable binding, since task creation is just the invocation of a function.

Task functions can be passed parameters, so they can construct their internal closure into a valid state by default, as the Average example (poorly) illustrates. This is the equivalent of object construction.

Tasks can encapsulate arbitrarily rich logic just like an object. Moreover, they can be composed and arranged into arbitrarily sophisticated structures using the existing rules of the language combined with message passing.

Internal state is perfectly and cleanly hidden by the fact that the closure is not required to expose any of its local variables, and in fact can only do so by means of a message.

The syntactical burden - on both the programmer and the language implementer (hey, that's me!) - is minimal.

Combined with the fully orthogonal language feature of inner functions, this approach makes it trivial to hide portions of an API surface. I still plan to provide a full Access Control List feature at some point which dictates how various protocols can interact.



So overall I feel good with this, which means it's time to open it up to feedback and poke a bunch of holes in it!

The Abominable Amalgam

11 November 2014 - 05:59 PM

I just wrote a tiny side-project thingy that used all of the following in one program:
  • Code compilation via libclang
  • Execution of generated code in-process
  • Dynamic link libraries invoked via parsing a string
  • Coroutines (Win32 fiber API)
  • longjmp
  • TerminateThread()
  • Thread-local attributes on static class members
  • A shitload of void*
I'm sure there's other terrible things in there. I just needed a confessional to get it off my chest.



I am an awful programmer.

Code organization without the limitations of files

10 September 2014 - 01:41 PM

I've been contemplating an interesting phenomenon recently, some of which has spilled out of my brain into my journal here on GDNet, but mostly it's been a lot of quiet brain-churning.

First a bit of background. Suppose we have a large project, like a decent sized game, and we need to explore the code. Consider the first time you dropped into a large, unfamiliar codebase, and what kinds of spelunking was necessary to learn how all the pieces fit together.

Usually, code is organized into conceptual units which I'll call modules. Modules consist of code units such as data structures, functions, type definitions, global variables/constants, and so on. These may or may not be able to be broken into namespaces, which are typically orthogonal to modules in terms of language semantics, although some languages treat modules and namespaces as identical concepts.

The real question is, how do we lay out all this stuff on disk? Traditionally we write code in exactly one place in exactly one file. This is great for some things, like diffs and version control, but sucks for other things - like discoverability.

Going back to the scenario from above, think about what happens when you get a project that contains a lot of code. Most of that code does not fall into exactly one category. You may have UI, graphics, physics, AI, audio, game-specific logic, and so on.


Now suppose I want to view all code related to casting a fireball spell in this particular game - head to toe, from the UI button that triggers the spell to the game logic that handles it to the graphics code that renders it. Throw in audio effects for good measure.

How do I see all this stuff in the traditional structure? I open thirty different files, each of which might have two or three code units I actually care about.

Now I need to add some logic about fireballs, but only fireballs, and it has to do with graphics and audio. I need to distribute my code changes across at least two files. And what if I have something that is related to both fireballs and lightning, but not much else? What file does it go into?


This all strikes me as extraordinarily wasteful (in terms of programmer time and energy). I'm curious if there is a better way to allow for organizing code, so that this kind of scenario goes away entirely. There are other benefits I can imagine to getting away from the 1:1 file religion, but they're mostly side effects.

The real question is: can we invent a way to think of and view code that elegantly solves this kind of problem?

Isn't "delete" so pesky to type? Wish there was a better way?

01 August 2014 - 01:09 PM

In C++, we know that memory management can be a large manual burden. Sometimes, though, it's just unavoidable, and we have to use new and delete.

A large part of the manual burden, of course, is actually typing delete, because it takes sooooo loooooong and is just annoying.


Thankfully, C++ has some special and little-known syntax to solve this headache! Suppose you have pointers (allocated via new, of course) named Foo, Bar, and Baz. All you need to do is this:

delete Foo, Bar, Baz;

Enjoy!

PARTNERS