||5 months ago|
|.github/workflows||6 months ago|
|.idea||1 year ago|
|.vscode||5 months ago|
|app||5 months ago|
|docs||9 months ago|
|installer||11 months ago|
|media||8 months ago|
|src||5 months ago|
|tests||9 months ago|
|tools||10 months ago|
|vendor||6 months ago|
|vmlib||8 months ago|
|.editorconfig||3 years ago|
|.gitignore||7 months ago|
|.gitmodules||11 months ago|
|.lldbinit||1 year ago|
|LICENSE.txt||2 years ago|
|README.md||8 months ago|
|config.nims||12 months ago|
|dist_config.example.json||7 months ago|
|enu.nimble||5 months ago|
|nimble.lock||5 months ago|
3D live coding, implemented in Nim.
Enu lets you build and explore worlds using a familiar block-building interface and a Logo inspired API.
It aspires to make 3D development more accessible, and will eventually be usable to create standalone games.
Note: The docs for Enu 0.2 are a work in progress. Most of the core ideas are here, but a fair bit of revision
is required. The 0.2 docs will be targeted towards new programmers, with 'Note for Nimians` sections aimed at more
experienced folks to explain what's really going on. However, things are all over the place right now, with the
intended audience changing paragraph by paragraph.
Notes for Nimians: Enu tries to simplify some Nim concepts, mainly to defer explaining unfamiliar terms. In particular,
Enu tries to hide most things related to types, calls procs 'actions', and avoids immutable variables. I believe this is
the right approach for new programmers, but I expect that more sophisticated developers will use a style closer to
Enu is meant for anyone who wants to explore, experiment, or make games, but particular care has been taken to make
it usable by younger people who may struggle with reading or typing. However, rather than bypassing the keyboard with
a Scratch-like visual programming language, Enu attempts to reduce and simplify the keystrokes required for a text-based
language, while (hopefully) preserving most of the flexibility text-based code offers.
With this in mind, Enu tries to:
Reduce nesting. Indentation can be tricky for new programmers.
Reduce the use of the shift key. Lower case is used almost everywhere. Commands are written to
avoid underscores and parenthesis. By default (for now at least), a
;keypress is interpreted as
:, as colons are
required frequently in Nim (and require shift, at least on US English keyboards) while semi-colons are not.
Omit or shorten identifier names.
for i in 0..5:. Single letter shortcuts for many common commands.
Pretends to avoid types. Enu code is Nim code and is statically typed, but a fair amount of effort has been spent
hiding this fact. Types are great, but are confusing for new programmers.
Spatial organization. No files. Code is text, but it's accessed through an object in the virtual world.
Avoids events. Tries to make all flow based on loops and conditionals.
Entities/objects in Enu are referred to as units, and have a base type of
Unit. Currently there are
(voxel objects) and
Bot units (the robot). There will be more in the future.
me is the unit that owns a script, and is equivalent to
this in other environments.
me was selected
because it's easier for a child to type.
me. can be auto-inserted when accessing properties of the unit. For example,
me.speed = 10 would commonly be written as
speed = 10. There are probably bugs with this behavior. Please report
Enu uses a prototype based object system for units. To allow a unit to be a prototype, you give it a name:
Then create a new instance in a different script with
var ghost2 = ghost.new
You can also provide parameters, which can be overridden when creating a new instance:
name ghost(color = white, speed = 5, spookiness = 10)
These become properties of the unit (ie
me.spookiness = 5), but can be treated like variables in the unit's script
due to auto
me insertion (
spookiness = 200).
To create a new instance with specific property values:
var ghost2 = ghost.new(spookiness = 11)
Parameters can have a default value (
spookiness = 10), which makes them optional when creating a new instance. If they
should be required, or there's no reasonable default value to use, specify a type (
spookiness: int) instead, or omit
both the value and the type, which will make the type
auto can be implicit,
name ghost(a, b: int)
treats parameters differently than
proc ghost(a, b: int) would. With the proc,
b are both
name version would make
global can always be passed to a new instance, even if the prototype name doesn't include them.
Generally, if an Enu command takes a number, it will be a
int will auto-convert to
when a numeric
Range is passed to something expecting a number, a random value within the range will be selected. So, even
forward expects a
float, the following are all valid:
forward 1.0 forward 1 forward 1.0..5.0 # Convert to a random float between 1.0 and 5.0 forward 1..5 # Convert to a random int between 1 and 5, then convert the int to a float
in operator can be used between two numbers to test for random chance. For example:
if 1 in 2: echo "I should be hit 50% of the time" if 1 in 100: echo "I should be hit 1% of the time"
By default random numbers in Enu are based partially on the time and will be different each time a script is executed.
However, sometimes you want randomness to create variety, but want the same values to be chosen each time a script is
run. This is especially important when using randomness in a
Build that you plan to manually edit later. To ensure the
same values are selected each time a script is run, set the unit's
seed property to some integer of your choosing,
seed = 12345 or
me.seed = 54321.
Any child units instanced by a unit with a seed value will get the same seed by default. However, it will still get
a unique random number generator, so changing the script for a child object won't impact the random numbers selected by
When dealing with a
Build unit, commands can do different things depending on whether the unit is in
move mode moves the unit around, while
build creates new blocks. By default a
Build is in
build mode. Often you'll pass the
me unit to
move/build, but it's also possible to pass other units. For example:
build me # generally not required, as it's the default # make a shape: back 5 right 3 # move it into position: move me up 3 # create another unit and add some blocks var enemy = ghost.new build enemy down 5 # move it into position move enemy up 5
It's also possible to call commands directly against a unit instance, but they will always use
move mode, regardless
of which mode is in use:
build enemy up 5 # build 5 blocks up enemy.up 5 # move up 5
Move or build x number of blocks in the specified direction. Defaults to 1 block.
forward 5 enemy.up 2
Turn a unit. Can be passed:
- a number in degrees. Positive for clockwise, negative for counter-clockwise. Ex.
- a direction (
forward/back/up/down/left/right) which will turn in that direction. 45 degrees by default.
turn left, or
turn up, 180
- a unit to turn towards. Ex.
- a negative unit to turn away from. Ex.
near(less_than = 5.0) / far(greater_than = 100.0)
Returns true or false if a unit is nearer/farther than the specified distance. For example:
if player.near: echo "the player is 5m or closer" if player.far: echo "the player is 100m or farther" if player.near(10): echo "the player is 10m or closer" if player.far(25): echo "the player is 25m or farther"
If a unit is touching another unit, return the vector of the contact. Defaults to testing against
me. For example:
if player.hit: echo "I'm touching the player" if player.hit == UP: echo "The player is on top of me" if player.hit(enemy1): echo "The player hit enemy1"
Gets or set the position of a unit as a Vector3.
me by default.
if player.hit(enemy): # if the player hits `enemy`, reset the player position to the center of the world. player.position = vec3(0, 0, 0)
The starting position of a unit. Missing currently, but will be in in 0.2.
Gets or sets the speed of a unit.
me by default.
While building, speed refers to the number of blocks placed per frame. In the future this will be normalized to 60fps,
but currently the speed is tied to the framerate. Setting speed to 0 will build everything at once.
While moving, this is the movement speed in meters per second.
Switching between build and move mode doesn't impact the speed, except in the case of switching to move mode from build
mode with a speed of 0.
speed = 0 is extremely common for build mode, but makes things appear broken in move mode,
as nothing will actually move, so switching to move mode with a speed of 0 will automatically reset the speed to 1.
Sets the scale/size of a unit.
me by default.
Specifies the glow/brightness of a unit.
me by default. Currently does nothing for bots, but will in the future.
Specifies if a unit is in global space, or the space of its parent. If
global = true and the parent unit moves,
child units are unaffected. If
global = false, the child will move with its parent. Does nothing for top level units,
as they're always global.
By default, new
Build units are
global = false and new
Bot units are
global = true.
Gets the rotation of a unit as a Vector3.
Gets or sets the velocity of a unit, as a Vector3. Currently buggy.
Gets or sets a units color.
me by default. For
Build units, this only impacts blocks placed after the property
is set. For
Bot units this does nothing, but in the future it will change their color.
Bounces a unit in the air. Currently only works for the player.
Build units only.
save the position, direction, drawing state, and color of the draw point, to
restore it later.
Can optionally take a name string to enable saving/restoring multiple points.
Instantly return unit to start position and resets rotation and scale.
Moves a unit to its start position via a
down sequence with appropriate values. Can fail if there
are obstructions along the way. Compare
start_position after running to test for success.
sleep(seconds = -1.0)
Do nothing for the specified number of seconds. If no argument is provided, or the argument is < 0, this will wait for
0.5 seconds or until unit is interrupted, which will end the
sleep prematurely. This allows the following:
forever: sleep() if player.hit: echo "ouch!"
Currently, any collision will trigger an interrupt. This will be expanded in the future.
Alternate between a list of values, returning the next element each time the cycle is called.
forever: sleep 1 echo cycle("one", "two", "three")
Many Enu command also have a 1 letter alias. These are harder to read, but can reduce friction for folks new to
The aliases are:
turn. Can be combined with shorthand directions, so
turn rightcan be expressed as
while true:(o was selected because its shape is a loop)
5.x:will run a code block 5 times.
# draw a cube (with no top) 10.x: 4.x: f 10 t r u 1
Procedures/functions in Enu are referred to as actions, mainly to avoid explaining the term procedure, subroutine, or
function, and to tie them to Action Loops defined below. Their syntax resembles markdown lists, and
have the same parameter rules as prototype names. That is, mostly the same
as Nim procs, but types can be omitted, making the parameter implicitly
- hello(name): echo "hello ", name - goodbye(name = "Vin"): echo "goodbye ", name hello "Claire" goodbye "Cal"
Action parameters are automatically shadowed by a variable with the same name and value, making them mutable within the
action. Enu tries to avoid the concept of immutable values.
It's also possible to specify a return type between the closing bracket of the parameter list and the colon:
- hello(name) string: "hello " & $name echo hello("Scott")
However, at this point it's probably better to use a
Note for Nimians: Action Loops are state machines, and any proc can be a state. If the proc has a return value it will
Action Loops can help control the complexity of the logic for your units. They allow you to run complicated lists of
actions and switch between them easily when situations change.
An Action Loop always has one and only one current action, which it will call repeatedly until you switch to some
other action. The default action is
nil. The first thing a loop must do is switch from
nil to another action, using
the little switch arrow
loop: nil -> forward # I'll go forward forever!
The little switch arrow (
->) will switch from the action on the left to the action on the right if it's encountered
and the left action has just completed. If the loop goes through and no switches match, the current action will be
We can control which switches get run by putting them in
loop: nil -> forward if player.far: forward -> back if player.near: back -> forward
In the above example, the loop immediately switches to
forward, and will go forward indefinitely until one of the
conditions is met and the action is switched to something else. If the player gets too far away (more than 100m) and
the action is
forward, the action will be switched to
back. If the player is near (5m) and the action is
it will switch to
forward. However, if the player is near and the action is
forward, nothing will change. The
if player.near statement will be true, but
back -> forward is ignored, since the current action isn't
If you want your loop to end at some point, you can switch back to
loop: nil -> forward(10) # Some actions can take additional parameters. forward -> nil # I'll run `forward(10)` a single time, then stop and end the loop.
Let's look at something more complicated, and introduce the big switch arrow (
==>) and change actions.
- wander: speed = 1 forward 5..10 turn -45..45 - charge: speed = 5 turn player forward 1 - flee: speed = 3 turn -player forward 1 - attack: player.bounce 5 turn 360 var health = 3 loop: nil -> wander if 1 in 50: # when each `wander` action finishes, there's a 1 in 50 (2%) chance of our unit getting a sudden # burst of energy and switching to the `charge` action. Otherwise we just keep wandering. wander -> charge if health == 0: # we died. Exit the loop. We want this to happen immediately, not after the action finishes, so we use # the big switch arrow. We use the special `any` action to say that this should happen regardless # of the running action. any ==> nil if player.hit: # if the player touches us while we're wandering, we flee. We want it to happen the instant the player touches us, # not when our current `wander` is done, so we use the big switch arrow wander ==> flee: # this is a change action. If the action switches here, the change action will also run once. health -= 1 # if the player touches us while we're charging, we attack immediately. charge ==> attack if player.far: # if we're fleeing the player, we go back to wandering when they get far away flee -> wander # Switch to wander when our attack is done. We always want this to happen, so it isn't in a # conditional. It only does anything if the current action is `attack`, and we only do it when # the attack is done becuase we're using the little switch arrow attack -> wander
Actions are generally just a simple lists of commands. It's fine to put logic in them, but anything complicated
will quickly get unwieldy. Imagine we have a unit that performs two complicated actions,
find_treasure might need to
navigate an area,
locate items of interest,
interact with them,
then return to
home_base to deposit them.
fight_monster could require actions like
Combining all of this in a single action loop would give us a lot of actions, many of which are mostly unrelated,
and managing our switches could get very complicated. However, making them entirely separate isn't ideal either, as
there's probably some common functionality between them (die if our health gets too low, respawn if we get stuck). We
also need to switch between the two actions.
A good way to manage this is by making
fight_monster child loops rather than regular actions.
We can treat them like regular actions in our main
loop, but when we switch to them they'll be able to perform
more sophisticated logic than a normal action could. In addition, our main loop will continue to run along side
the the child loop, so we can quickly switch out of the child loop with a big switch arrow
==> in response to
certain conditions, without either loop needing to worry about higher level concerns.
Our main loop could look something like this:
loop find_treasure: # our treasure logic goes here. This loop doesn't have an exit condition. if treasure_found: # We found it. Start looking again. This will switch from any action apart # from `look_for_chest` others ==> look_for_chest loop fight_monster: # fight logic here. This loop should exit when the monster dies if monster.dead: any ==> nil loop: # We want our unit to find_treasure indefinately. `find_treasure` doesn't exit (switch to nil) nil -> find_treasure if monster.near: # find_treasure doesn't need to know anything about monsters. We can break out of it # with a big switch arrow if we encounter one. find_treasure ==> fight_monster # fight_monster does have an exit condition (the monster dies), so we can wait for it to finish # using the little switch arrow, then go back to finding treasure. fight_monster -> find_treasure if health == 0: # if our health drops to 0, it doesn't matter what else we're doing. Die immediately. Break out # of any action (except die) with a big switch arrow. (any, -die) ==> die # this would also work: # others ==> die if stuck: # respawn if we're stuck, but only from our two child loops. We don't want to respawn if we're # already respawning, or we're dead. Implementation of `respawn` not shown. (find_treasure, fight_monster) ==> respawn # we're done respawning. Treasure time! respawn -> find_treasure
Child loops can also call other child loops, in which case both the parent and grandparent loops can use
==> to break
out of the top level loop. There's no set limit to nesting depth.
-> Little Switch Arrow
Switches from one action to another, after the first action has finished running.
draw_box -> draw_stairs
==> Big Switch Arrow
Switches from one action to another immediately. Will interrupt the running action.
if player.near: sleep ==> offer_quest
More about Action Loops
Actions are just procedures, and they can take parameters. Sometimes you want to run the same action
in different situations with a different action name. You could do this by creating a new action that
calls the first one, but you can also use
as to give an action a different name.
# explore action and health var not shown. - flee(distance = 100): turn -player forward distance loop: nil -> explore if player.near and health > 2: explore -> flee elif player.near and health <= 2: explore ==> flee(200) as really_flee if player.far: flee -> explore if player.far(150): really_flee -> explore
Special from actions
Often loops will switch from a single action to another. However, sometimes you want to allow switching from
a variety of actions.
any -> some_action- switch from any action to the target action. This will switch (and run any change action)
even if we're already running the target action. In this example we used a little switch arrow, so it still won't
happen until the current action actually completes.
others -> some_action- Same as
any, but it excludes the target action.
(action1, action2)- Multiple from actions can be supported by putting them in a tuple.
(any, -action2, -action3)- Switch from any action except
When do action loops run?
An action loops will run whenever its executing action finishes. In addition, action loops will run
every 0.5 seconds, and when something triggers an interrupt. Currently only the start and end of a collision with the
player trigger an interrupt, but this will be expanded.
When using child loops, the top level loop runs first, then walks down the stack of loops until the currently executing
loop is reached.
TODO: Include examples of new 0.2 functionality
Draw a square:
forward 10 right 10 back 10 left 10
4.times: forward 10 turn_right()
var length = 20 height = 50 height.times: left length / 2 back length / 2 4.times: forward length turn right forward length / 2 right length / 2 turn 5 up 1
up 10 forward 10 (50..100).times: forward 2..5 turn -180..180 up 0..2
Set the color to blue randomly with a 1 in 50 chance. Otherwise set it to white:
if 1 in 50: color = blue else: color = white
or as a one-liner:
color = if 1 in 50: blue else: white
Move forward 10 times, cycling through colors:
10.times: color = cycle(red, black, blue) forward 1
Download from https://github.com/dsrw/enu/releases. The Windows version isn't signed, and
UAC will warn that it's untrusted. This will be fixed in a future release.
The Linux version hasn't been tested particularly well, but it works for me under Ubuntu 20.04. Please report any issues.
The world format will change in a future release. Worlds created in 0.1 won't be supported in future versions.
Build and Run
$ nimble prereqs $ nimble build $ nimble import_assets $ nimble start
Enu requires a custom Godot version, which lives in
vendor/godot. This will be fetched
and built as part of
ESC- toggle mouse capture and to dismiss editor windows. Reloads script changes.
W, A, S, D- move around when mouse is captured.
Space- jump. Double jump to toggle flying. Hold to go up while flying.
C- go down while flying.
~- toggle the console.
F- toggle fullscreen.
1- enter edit mode.
2 - 9- change active action.
Mouse Wheel Up/Down- change active action.
Alt- reload script changes. Hold to temporarily capture the mouse and move, so you can
change your view without having to switch away from what you're doing.
Cmd+P / Ctrl+P- Pause scripts.
Cmd+Shift+S / Ctrl+Shift+S- Save and reload all scripts, then pause. If you have a script that makes a unit
inaccessible (ex. moves the unit below the ground) this is a way to get things back to their start positions so they
can be edited.
Left Click- Place a block/object or open the code for the currently selected object.
Right Click- Remove a block/object.
XBox / Playstation Controller
Left Stick- move.
Right Stick- change view.
A / X- jump. Double jump to toggle flying. Hold to go up while flying.
B / ◯- go down while flying. Dismiss code editor.
Y / △- toggle edit mode.
L1 / R1- change active action.
L2- place a block/object or open the code for the currently selected object.
R2- remove a block/object.
Enu currently includes 6 block types/colors, and 1 object model (a robot). This will be greatly
expanded in the future.
Drop a block or robot with the left mouse button/controller trigger, remove it with the right. Adjoining blocks will be
combined into a single structure. With the mouse captured, building works more or less like MineCraft. Release the mouse
by pressing ESC to draw blocks using the mouse cursor.
Code by switching to the code tool by left clicking/triggering on an object or structure. Changes are applied when the
code window is closed (ESC key) or CTRL is pressed. Holding CTRL will also temprarly grab the mouse and allow you to
change your position.
The Enu data directory lives in
~/Library/Application Support/enu on Mac,
%AppData%\enu on Windows, and
~/.local/share/enu on Linux.
config.json has a few configurable options:
mega_pixels: The render resolution, in mega pixels. Increase for more detail. Decrease for better performance.
font_size: The font size. DPI is currently ignored, so hidpi screens will require a higher number.
dock_icon_size: Size of the icons in the dock. DPI is currently ignored, so hidpi screens will require a higher number.
world: The world/project to load. Change this to create a new world.
show_stats: Show FPS and other stats.
start_full_screen: Whether to start Enu full screen, or in a window.
: will be interpreted as
: to be typed without shift. Sometimes useful for new typists.
TODO for 0.2
Currently it isn't possible to change the pivot point for a unit, and the default point isn't properly centered for
most builds, making it difficult to rotate builds nicely. Enu 0.2 will use the draw point for the pivot point, allowing
it to be moved, and will shift everything over 0.5m, allowing most builds to rotate in a balanced way. There will
also be a command to move the draw point (and thus the pivot point) in the exact center of a build.
We need a way to switch worlds without editing a config file. Adding a REPL may be the easiest way to accomplish
this, and is something I wanted to add anyway.
Testing and bug fixes
Enu has been under heavy development for a year without a great deal of testing, so there are undoubtedly bugs. I
believe these will be minor, but there are probably a fair number of them.
v0.2.x - v0.3
- iOS support.
- Move script execution off the main thread.
- Settings UI
- Allow the editor pane and action bar to be resized from within Enu.
- Better collision support
- Blocks of any color
- In game help
- Easy way to switch worlds in-game
- Support loading worlds from anywhere, not just the Enu data directory