EmptyEpsilon

What is it?

EmptyEpsilon is a spaceship bridge simulator game. It's fully open source, so it can be modified in any way people wish.

What does this mean?

EmptyEpsilon places you in the roles of a spaceship's bridge officers, like those seen in Star Trek. While you can play EmptyEpsilon alone or with friends, the best experience involves 6 players working together on each ship.

Each officer fills a unique role: Captain, Helms, Weapons, Relay, Science, and Engineering. Except for the Captain, each officer operates part of the ship through a specialized screen. The Captain relies on their trusty crew to report information and follow orders.

Like Artemis Spaceship Bridge Simulator?

Artemis Spaceship Bridge Simulator was the inspiration for EmptyEpsilon. It's pretty good as a bridge simulator, but we had some issues with it that we wanted to fix. Since Artemis isn't open source, our only solution was to start over by ourselves.

For example, the "comms" station of Artemis is pretty limited and can be boring for the player. The game could fall out of sync, confusing the players. Also, its Game Master screen didn't do everything that we wanted.

All in all, Artemis is a nice game. We wrote our own to be able to implement new features and extensibility.

Contact

The best place to talk about bridge simulation and EmptyEpsilon is the Starship Bridge Simulation Network. We regularly check this forum, and post updates and battle logs of our space adventures.

The main developers of EmptyEpsilon go by the names of Daid and Nallath.

Donations

EmptyEpsilon is a 100% free game. We made this because we wanted to play the game we envisioned for ourselves, but we are not artists, so we need to buy the game's 3D models.

We are raising money to buy: Not sure yet (we still have cash reserve)

Previous contributions

Thanks to the contributions of Daniel Ginat, we managed to buy: more frigate models.

Thanks to the generous contribution of Serge Wroclawski, we managed to buy: more battleship models.

Thanks to the contributions of MadKat and Daid, we managed to buy: larger 3D station models.

To run a standard bridge for EmptyEpsilon, you need the following equipment:

  • 4-6 devices, 1 for each officer except the captain. These can be laptops, desktops, or Android devices.
  • One big screen (large monitor, TV, or projector).
  • A stable network. Wifi can work, but we recommend a wired LAN.
  • If playing over the internet, voice chat is strongly recommended.

All but 1 of the officers operate "stations" that control different parts of the ship. The last computer is for the main screen, which does not need any input and should be visible to all players.

Designate 1 player as Captain, whose computer can serve as the ship's main screen. The Captain's only duty is to communicate with the other officers and tell them what they should do.

The other officers each operate one of the ship's stations, which are described in the Stations tab.

Running the game

After starting the game on the computer serving as the main screen, you can also configure it as the game server. The other stations can then connect to the game server as clients.

After starting the server, select a scenario (we recommend the "Waves" or "Basic" scenarios for first games), then spawn a player ship for everyone to join.

Everyone should select the same ship and choose their designated station. That's it! You're now in command of a fully functional spaceship.

Internet play

A crew can play EmptyEpsilon over internet connections. This means the game server needs to be accessible from the internet. The server runs on port 35666, which needs to be open in your firewall and forwarded to your server if you are behind a router.

If you don't have enough players (or stations), you can run multiple stations on a single machine by selecting multiple roles in the ship selection menu. You can then switch between stations by pressing the button in the top right of the game screen.

There are also 3 special screens designed for crews of 3-4 officers: Tactical, Engineering+, and Operations.

  • Tactical combines helms and weapons into one station.
  • Engineering+ takes shields activation from the weapon station.

  • Operations acts the same as the science station, but also allows for communications.

These 3 stations can also be combined with the 5 standard stations.

A Game Master can modify scenarios on the fly by adding and removing AI-operated ships and stations, changing their configurations, placing space hazards, and communicating with player ships, all making for a more interesting and customized experience for players.

Scenarios can also add special GM functions that trigger scripted events. This lets Game Masters build complex stories and intricate, dynamic situations that they can manipulate at will during gameplay. Scripting commands are covered in depth on the Mission Scripting tab.

Without direct control of the ship, the Captain keeps the crew focused on their goal and makes tactical decisions in combat. The ship's main screen should be set up on a large monitor or projector so that all players can track their ship's status.

The Captain's tasks include:

  • Planning the next actions
  • Co-ordinating combat tactics
  • Preventing mutiny
  • Setting priorities

Controls

There are many ways to configure the main screen and define the Captain's role. You can grant any officer's station control over the main screen, but you can also give the Captain direct control over it with these keyboard controls:

  • Left mouse button: Rotate the main screen 90 degrees to the left
  • Right mouse button: Rotate the main screen 90 degrees to the right
  • Middle mouse button: Switch between ship, short-range radar, and long-range radar views
  • Up key: Set main screen to forward view
  • Down key: Set main screen to rear view
  • Left key: Set main screen to left view
  • Right key: Set main screen to right view
  • Tab key: Set main screen to short-range radar view
  • Q key: Set main screen to long-range radar view
  • F key: Set main screen to first-person view

Note that these controls are optional and are not necessary to play the game, but could be used in custom hardware setups. These controls are only for the main screen.

A "custom" hardware setup for the Captain could be as simple as taping a 3-button mouse to an armchair.

Helms

Data: In the upper-left corner, the Helms officer's screen displays the ship's energy (max is 1,000), current heading in degrees, and current speed in Units/minute. Below this data are two sliders.

Engines: The left slider controls the impulse engines, from -100% (full reverse) to 0% (full stop) to 100% (full ahead). The right slider controls the ship's high-speed warp or instantly teleporting jump drives, if the ship is equipped with either.

Setting a Heading: The Helms officer has a short-range radar. Pressing inside this radar sets the ship's heading in that direction. If the ship has beam weapons, the radar view includes those weapons' firing arcs to help the Helms officer keep targets in the Weapons officer's sights.

Jumping: A jump drive teleports the ship across the specified distance along its current heading. The ship's impulse engines shut down, and after a countdown the ship disappears from its position and instantly reappears at its destination. Each jump consumes energy, with longer jumps consuming more energy. A standard jump takes 10 seconds to initiate, but depending on how much power is allocated to the drive (and how damaged it is), the time to power the jump might vary.

Warping: A warp drive propels the ship straight ahead several times faster than impulse engines, but drain energy at a much faster rate. A warping ship can still collide with hazards like asteroids and mines, but a ship can enter warp very quickly for rapid escapes and advanced tactical maneuvers.

Combat Maneuvers: For ships capable of performing combat maneuvers, the Helms screen includes up to two special sliders at the bottom right. The vertical slider rapidly increases the ship's forward speed above its maximum cruising speed, but generates lots of heat in the impulse engines and consumes energy quickly. The horizontal slider moves the ship laterally but can quickly overheat the maneuvering system. Combat maneuvers can be exhausted but recharge over time.

Docking: The Helms officer can dock with a friendly or neutral station (or in some cases, a larger ship) when it is within 1U. While docked, the ship can't engage its engines or fire weapons, but its energy recharges faster, repairs take less time, the ship's supply of probes is replenished, and the Relay officer can request missile weapon rearmament. The Helms officer is also responsible for undocking the ship.

Retrieving Objects: The Helms officer is also responsible for piloting the ship into supply drops and other retrievable items to retrieve them.

Weapons

Data: In the upper-left corner, the Weapons officer's screen displays the ship's energy (max is 1,000), and the strength of its front and rear shields.

Targeting: To fire beam weapons and target guided missile weapons, the Weapons officer can select ships on the screen's short-range radar.

Missiles: Missiles are one of a ship's most destructive weapons. Before a missile can be fired, the Weapons officer selects it, then selects one of the weapon tubes to load it. Loading and unloading weapon tubes takes time. (Mines are also be loaded into a special type of weapon tube.) Weapon tubes face a specific direction, and some ships only have tubes on certain sides of a ship, making cooperation with the helms officer's maneuvers especially important.

To fire a missile, the Weapons officer presses a loaded missile tube. Except for HVLIs, missiles home in on any target selected by the Weapons officer. Otherwise, the missile is dumb-fired and flies in a straight line from its tube. The Weapons officer can choose to lock the tube's aim onto a target or click the Lock button to the top right of the radar to manually angle a shot.

There are several types of missile weapons:

  • Homing: A simple, high-speed missile with a small warhead.
  • Nuke: A powerful homing missile that deal tremendous damage to all ships within 1U of its detonation.
  • Electromagentic Pulse (EMP): A homing missile that deal powerful damage to the shields of all ships within 1U of detonation, but don't damage physical systems or hulls.
  • High-velocity Lead Impactor (HVLI): A group of 5 simple lead slugs fired in a single burst at extremely high velocity. These bolts don't home in on an enemy target.
  • Mine: A powerful, stationary explosive that detonates when a ship moves to within 1U of it. The explosion damages all objects within a 1U radius.

Beam Weapons: The location and range of beam weapons are indicated by red firing arcs originating from the players' ship. After the Weapons officer selects a target, the ship's beam weapons will automatically fire at that target when it is inside a beam's firing arc. The officer can use the frequency selectors at the bottom right, along with data about a target's shield frequencies provided by the Science officer, to remodulate beams to a frequency that deals more damage. Note that you can change the beam frequency instantaneously.

Beam weapons fire at a target's hull by default, but the Weapons officer can also target specific subsystems to disable an enemy. If you simply wish to destroy an enemy, however, it's best left on hull.

Shields: The Weapons officer is responsible for activating the ship's shields and modulating their frequency. It might be tempting to keep the shields up at all times, but they drain significantly more power when active. Certain shield frequencies are especially resistant to certain beam frequencies, which can also be detected in targets by the Science officer. Unlike beam weapons, however, remodulating the shields' frequency brings them offline for several seconds and leaves the ship temporarily defenseless.

Engineering

Power Management: The Engineering officer can route power to systems by selecting a system and moving its power slider. Giving a system more power increases its output. For instance, an overpowered reactor produces more energy, overpowered shields reduce more damage and regenerate faster, and overpowered impulse engines increase its maximum speed. Overpowering a system (above 100%) also increases its heat generation and, except for the reactor, its energy draw. Underpowering a system (below 100%) likewise reduces heat output and energy draw.

Coolant Management: By adding coolant to a system, the Engineering officer can reduce its temperature and prevent the system from damaging the ship. The ship has an unlimited reseve of coolant, but a finite amount of coolant can be applied at any given time, so the Engineering officer must budget how much coolant each system can receive. A system's change in temperature is indicated by white arrows in the temperature column. The brighter an arrow is, the larger the trend.

Repairs: When systems are damaged by being shot, colliding with space hazards, or overheating, the Engineering officer can dispatch repair crews to the system for repairs. Each systems has a damage state between -100% to 100%. Systems below 100% function suboptimally, in much the same way as if they are underpowered. Once a system is at or below 0%, it completely stops functioning until it is repaired. Systems can be repaired by sending a repair crew to the room containing the system. Hull damage affects the entire ship, and repair crews can always repair it, but hull repairs progress very slowly.

Science

Long-range Radar: The Science station has a long-range radar that can locate ships and objects at a distance of up to 25U. The Science officer's most important task is to report the sector's status and any changes within it. On the edge of the radar are colored bands of signal interference that can vaguely suggest the presence of objects or space hazards even further from the ship, but it's up to the Science officer to interpret them.

Scanning: You can scan ships to get more information about them. The Science officer must align two of the ship's scanning frequencies with a target to complete the scan. Most ships are unknown (gray) to your crew at the start of a scenario and must be scanned before they can be identified as a friend (green), foe (red), or neutral (blue). A scan also identifies the ship's type, which the Science officer can use to identify its capabilities in the station's database.

Deep Scans: A second, more difficult scan yields more information about the ship, including its shield and beam frequencies. The Science officer must align both the frequency and modulation of each scan type to complete a deep scan. The helms and weapons screen can also see the firing arcs of deep-scanned ships, which help them guide your ship from being shot by their beams.

Nebulae: Nebulae block the ship's long-range scanner. The Science officer cannot see what's inside or behind them, and while in a nebula the ship's radars cannot detect what's outside of it. These traits make nebulae ideal places to hide for repairs or stage an ambush. To avoid surprises around nebulae, relay information about where you can and cannot see objects to both the Captain and the Relay officer.

Probe View: The Relay officer can launch probes and link one to the Science station. The Science officer can view the probe's short-range sensor data to scan ships within its range, even if the probe is far from the ship's long-range scanners or in a nebula.

Database: The Science officer can access the ship's database of all known ships, as well as data about weapons and space hazards. This can be vital when assessing a ship's capabilities without a deep scan, or for help navigating a black hole, wormhole, or other anomaly.

Relay

Sector Map: The Relay station can view a map of the sector, including space hazards and ships within short-range scanner range (5U). It can also see the short-range sensor data around other friendly ships and stations, potentially spotting distant ships before the science station does. The Relay officer cannot scan ships, however.

Probes: The Relay officer can launch up to 8 high-speed probes to any point in the sector. These probes fly toward a location and transmit short-range sensor data to the ship for 10 minutes. Probes work inside nebulae, and thus are powerful tools when faced with an area blocked by nebula. The Relay officer can also link a probe's sensors to the Science station, which lets the Science officer scan ships within the probe's sensor range even if the probe is beyond the ship's long-range scanners. Probes cannot be retrieved and can be destroyed by enemies; your ship's stock of probes can be replenished only by docking at a station.

Waypoints: The Relay officer can set waypoints around the sector. These waypoints appear on the Helms officer's short-range scanner and can guide the ship toward a destination or on a specific route through space. Waypoints are also necessary when requesting aid from friendly stations.

Communications: The Relay officer can open communications with stations and other ships. Friendly ships hailed by the Relay officer can take orders, and friendly stations can dispatch backup and supply ships. While your ship is docked at a station, the Relay officer can request rearmament of the ship's missiles and mines. Some of these requests can cost some of your crew's reputation, which is also tracked by the Relay station.

About this series

This series documents the development of the EmptyEpsilon scenario Beacon of Light, which is included with the game.

EmptyEpsilon uses scenarios scripted in the Lua scripting language, which allows for many types of scenarios: basic sandboxes, story-driven campaigns, randomized enemies, and even dynamic branching dialogue. These scenarios are located in the "scripts" folder where EmptyEpsilon is installed.

The Beacon of Light scenario begins with instructions for the players to follow, then a series of challenges for them to overcome.

The scenario scripting system can be used to create even more complex scenarios, but this guide won't cover such features. All of the scenarios are viewable and editable, however, and you can look at the "Waves" scenario in the scripts folder to see an example of complex randomized events. If you downloaded the official EmptyEpsilon build or compiled it yourself, it also comes with a scripting guide that lists the available functions.

You can follow the first parts of this guide even if you have little or no programming knowledge. Later sections cover more advanced topics.

Tools

For scenario development, you need a basic text editor. While Windows Notepad can work, I highly recommend something more advanced.

There are many options, including these free editors:

I use Programmer's Notepad, but any of these editors will do.

1. Setting up the scenario file

We'll start with an empty scenario as a template.

Open your text editor and enter the following code:

-- Name: Beacon of light series -- Description: The beacon of light scenario, build from the scripting tutorial at https://daid.github.io/EmptyEpsilon/#tabs=4. --- Near the far outpost of Orion-5, Exuari attacks are increasing. A diplomat went missing, and your mission will start with recovering him. -- Type: Mission -- Init is run when the scenario is started. Create your initial world function init() -- Create the main ship for the players. player = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Atlantis") end function update(delta) end

Save this file as scenario_99_tutorial.lua in the folder EmptyEpsilon/scripts.

Open EmptyEpsilon and start a new server. Your scenario should be at the bottom of the list. Select the scenario and start it, and notice that there a ship for the players is already present.

Tips & Tricks

  • The file must start with scenario_ and must be located in the scripts folder for EmptyEpsilon to recognize it as a scenario.

  • The number in the scenario filename sets the order in which EmptyEpsilon shows the scenarios. The final part of the filename is for reference only.

  • The scenario name and description displayed in the game are set by the -- Name: and -- Description: lines at the beginning of the scenario file. For multi-line descriptions, each line should immediately follow the previous line and start with three dashes ---.

  • Other lines starting with a double dash -- are comment lines in the scenario script that you can use for your own reference. The game ignores them.

  • To quickly examine the scenario, you can use the Game Master screen listed with the Alternative Options on the ship selection menu. This lets you view and modify the entire game world with ease.

Building a universe

Now that you have setup a basic script and know how to save and run it, let's populate the universe a bit more.

First, let's fill it with some stations, and then add some nebulae and asteroids.

Adding stations

The only thing in our universe is the player ship. This doesn't make for a very interesting universe, so let's add some space stations that they can dock and interact with.

When looking at your scenario from the Game Master screen, the starting point is located at the top left corner of sector F5. These coordinates are "0, 0", with the X coordinate first and the Y coordinate second. If you click on the player ship on the Game Master screen, you can see its coordinates in the top left corner.

Note as you move up/north/on heading 0, your Y coordinate decreases; locations north of the starting point have a negative Y coordinate. Moving right/east/on heading 90 increases your X coordinate. Also note that in-game distances are measured in units (U), while scripts measure distances in milliunits -- 1U in the game is equal to 1000 in a script. See this image for an example.

Add the following code after the creation of the player ship:

SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100)

This line places a small Human Navy station 23.5U (23500) to the right of the starting point at sector F5, and 16.1U (16100) down from the starting point. Since each sector is a 20U square, this places the station in sector F6. The player ship also belongs to the Human Navy faction, so the station will be friendly to the players.

Next, add these lines to add three more stations:

SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200) SpaceStation():setTemplate("Large Station"):setFaction("Exuari"):setPosition(-45600, -15800) SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400)

This adds another friendly station in sector G3 (25.2U left and 32.2U down from the starting point), a large enemy Exuari station in sector E2 (45.6U left and 15.8U up from the starting point), and a small neutral station in sector D5 (9.1U right and 35.4U up from the starting point).

Now, we want the player to start closer to the small friendly station. So we set the player position near the small station.

PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis"):setPosition(22400, 16200)

Creating nebulae

Nebulae are interesting additions to a universe. They block a 5U circle on long-range radar as well as the space behind it, which gives players and enemies room to hide and provide interesting tactical options to the game.

Let's surround the enemy station with some nebulae so that it's shielded from sensors.

--Nebula that hide the enemy station. Nebula():setPosition(-43300, 2200) Nebula():setPosition(-34000, -700) Nebula():setPosition(-32000,-10000) Nebula():setPosition(-24000,-14300) Nebula():setPosition(-28600,-21900)

Such a cloud of nebulae clearly hides something, so let's also add a few random nebulae.

--Random nebulae in the system Nebula():setPosition( -8000,-38300) Nebula():setPosition( 24000,-30700) Nebula():setPosition( 42300, 3100) Nebula():setPosition( 49200, 10700) Nebula():setPosition( 3750, 31250) Nebula():setPosition(-39500, 18700)

Creating asteroids

You can add an asteroid in a similar manner as nebulae and stations:

Asteroid():setPosition(-1000, -1000)

However, because you usually want belts of 50, or even 100, asteroids, placing them all by hand would take quite a bit of time!

Fortunately, we can use Lua functions to help add many asteroids quickly. First, let's use the random() function to place an asteroid in a random location of sector E5. Because sector E5 is one sector to the left of sector F5, the range is from 0 to -20000 on the X axis and 0 to 2000 on the Y axis.

Asteroid():setPosition(random(0, 20000), random(-20000, 0))

Now let's use a loop to repeat this 100 times..

--Create 100 asteroids for asteroid_counter=1,100 do Asteroid():setPosition(random(0, 20000), random(-20000, 0)) end

This is a for loop. It counts from 1 to 100 and performs the action inside once for each count. In this case, it adds an asteroid to a random position in sector E5.

Which... results in a pretty square asteroid field. Not that pretty.

So right now, let's tweak the parameters to create a field from sector E4 to G4 in a straight line.

--Create 50 asteroids for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end

That's already much better. But from a 3D view, this asteroid belt looks very flat. To fix that, add some VisualAsteroids as well. These asteroids are above or below the ship's plane and can't be hit. They're only there for visuals.

--Create 100 asteroids for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) VisualAsteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end

Spice it up a bit

The stations have automatically assigned callsigns, such as DS-7 and other DS-? numbers. We can change these through the scenario scripts.

By calling the setCallSign() function on the stations, we can set a different name for each of them. Keep the callsign short.

SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100):setCallSign("Research-1") SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200):setCallSign("Orion-5") SpaceStation():setTemplate("Large Station"):setFaction("Exuari"):setPosition(-45600, -15800):setCallSign("Omega") SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400):setCallSign("Refugee-X")

We'll call the small station a research station, which gives it a bit more character. The enemy station is called Omega, as it's big. And the neutral station is called Refugee-X, which makes it sound like it could house refugees. These few simple names give the whole universe more character.

Finally, let's christen the player ship the Epsilon.

PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis"):setPosition(22400, 18200):setCallSign("Epsilon")

Tips & Tricks

In addition to asteroids, stations, and nebulae, you also have other options to make your universe more exciting:

BlackHole() -- Creates a 5U-radius black hole that drags anything that comes toward its center and destroys it. WormHole():setTargetPosition(20000, -20000) -- Creates a 5U-radius wormhole that leads to another point in space. Mine() -- A simple mine causes a 1U-radius explosion when a ship comes too close. WarpJammer():setFaction("Exuari") -- A warp jammer, which jams warp and jump drives of other factions in a 5U radius. Can be destroyed. SupplyDrop():setFaction("Human Navy") -- A supply drop, which a friendly player ship can pick up by flying over it. -- Supply drops are empty by default but can be filled with energy and missile weapons through additional script parameters.

Compiled example

This is the script so far:

-- Name: Beacon of light - Day 1 -- Description: The beacon of light scenario, build from the scripting tutorial at https://daid.github.io/EmptyEpsilon/#tabs=4. --- Near the far outpost of Orion-5, Exuari attacks are increasing. A diplomat went missing, and your mission will start with recovering him. -- Type: Mission -- Init is run when the scenario is started. Create your initial world function init() -- Create the main ship for the players. PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis"):setPosition(22400, 18200):setCallSign("Epsilon") SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100):setCallSign("Research-1") SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200):setCallSign("Orion-5") SpaceStation():setTemplate("Large Station"):setFaction("Exuari"):setPosition(-45600, -15800):setCallSign("Omega") SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400):setCallSign("Refugee-X") --Nebulae that hide the enemy station. Nebula():setPosition(-43300, 2200) Nebula():setPosition(-34000, -700) Nebula():setPosition(-32000,-10000) Nebula():setPosition(-24000,-14300) Nebula():setPosition(-28600,-21900) --Random nebulae in the system Nebula():setPosition( -8000,-38300) Nebula():setPosition( 24000,-30700) Nebula():setPosition( 42300, 3100) Nebula():setPosition( 49200, 10700) Nebula():setPosition( 3750, 31250) Nebula():setPosition(-39500, 18700) --Create 100 asteroids for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) VisualAsteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end end function update(delta) end

Making friends and enemies

Now we'll add some things that really spice things up: other ships.

About other ships

When working with other ships, remember a few basic things:

  • Factions: A ship's faction defines its relationship with other ships. For example, ships of the Exuari faction attack ships and stations of the Human Navy faction, and vice versa.
  • Orders: Every computer-controlled ship has orders that define how the ship behaves. A ship can be ordered to stay still, attack a target, defend a location, or seek out targets on its own.
  • Templates: Every ship's appearance, equipment, and base statistics are drawn from a template. A script can override nearly any trait in a template, except for the ship's model.

Your first enemy

Let's create an enemy.

CpuShip():setShipTemplate("Adder MK5"):setFaction("Exuari"):setPosition(1000, 1000)

Fight it! I'm sure you'll win -- notice that it doesn't do anything, not even respond to your attacks. This is because the default orders for a CpuShip are to stay idle and do nothing.

Let's give it one of the most basic orders: to seek new targets and attack them.

CpuShip():setShipTemplate("Adder MK5"):setFaction("Exuari"):setPosition(1000, 1000):orderRoaming()

Now you have an enemy to fight!

Let's start setting up the scenario's opposition with 2 cruisers at the lower nebula. They will fly out and seek the player quite well on their own.

--Small Exuari strike team, starting in the nebula at G5. CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3550, 31250):orderRoaming() CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3950, 31250):orderRoaming()

Defending the station

The Exuari have a large station in this area, so let's set up some defence.

We'll create 3 ships to defend Omega station. This means we need a way to refer to the station in our script. Edit the lines for Omega station to assign it to a variable.

-- Grab a reference to Omega station. enemy_station = SpaceStation():setTemplate("Large Station"):setFaction("Exuari") enemy_station:setPosition(-45600, -15800):setCallSign("Omega")

Note how with a variable as a reference, we can move the setPosition() and setCallSign() functions to a new line. Let's also use this reference to assign new ships to its defense.

-- Create the defense for the station CpuShip():setTemplate("Starhammer II"):setFaction("Exuari"):setPosition(-44000, -14000):orderDefendTarget(enemy_station) CpuShip():setTemplate("Phobos T3"):setFaction("Exuari"):setPosition(-47000, -14000):orderDefendTarget(enemy_station) CpuShip():setTemplate("Atlantis X23"):setFaction("Exuari"):setPosition(-46000, -18000):orderDefendTarget(enemy_station)

With the DefendTarget orders, the ships will circle around the target and attack any enemies in range. You can set ships to defend any object, including another ship.

For example, we can setup two extra fighters which follow and defend the Atlantis X23.

enemy_dreadnought = CpuShip():setShipTemplate("Atlantis X23"):setFaction("Exuari") enemy_dreadnought:setPosition(-46000, -18000):orderDefendTarget(enemy_station) CpuShip():setShipTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18100):orderDefendTarget(enemy_dreadnought) CpuShip():setShipTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18200):orderDefendTarget(enemy_dreadnought)

You lose! (Or, you should be able to)

When the player ship is destroyed, or when all enemies are destroyed, nothing happens. Let's fix this.

First, we want to end the game with defeat if we lose the player ship. We need to edit the lines that add the player ship to create a reference to it.

player = PlayerSpaceship():setFaction("Human Navy"):setShipTemplate("Atlantis") player:setPosition(22400, 18200):setCallSign("Epsilon")

Next, we'll finally use the update() function that has been empty so far. This function is special: the game runs any code in the update function every game tick, or about 60 times per second. Let's use that to constantly check whether the player ship is destroyed, and if it is, then end the game.

The isValid() function checks an object and returns a value of either true or false: true if the object exists, false if it doesn't. With a conditional statement (also called an if statement), we can ask, "Is the player ship not a valid object?", and if the response is true (the player doesn't exist), we can tell the game to declare victory for the Exuari. Doing so in the update function asks that question repeatedly.

function update(delta) --When the player ship is destroyed, call it a victory for the Exuari. if not player:isValid() then victory("Exuari") end end

Pretty simple, right? And naturally, you can setup victory for the player the same way by checking Omega station.

--When Omega station is destroyed, call it a victory for the Human Navy. if not enemy_station:isValid() then victory("Human Navy") end

Tips & Tricks - More about ships

  • For all possible ship templates, check the files starting with shipTemplates_ in the scripts folder. These files contain all of the standard ship definitions.
  • In addition to orderRoaming and orderDefendTarget, CpuShips can also have these orders:
    • orderIdle(): Default state. Do not move or attack.
    • orderStandGround(): Hold this position, but attack enemies when they are in range. The ship will fire missiles as well as beam weapons.
    • orderDefendLocation(x, y): Fly toward the given position and defend it from attacks.
    • orderFlyFormation(target, offset_x, offset_y): Fly in formation with the target by keeping a certain offset. When enemies are near the target, engage them. Gives fleet behaviour.
    • orderFlyTowards(x, y): Fly toward a target position and attack enemies when they come too close.
    • orderFlyTowardsBlind(x, y): Fly toward a target position and ignore everything else.
    • orderAttack(target): Attack a specific target.
    • orderDock(target): Dock with a target. Can be used to make computer-controlled ships dock with stations.

Compiled example

This is the script so far:

-- Name: Beacon of light series - Day 2 -- Description: The beacon of light scenario, build from the scripting tutorial at https://daid.github.io/EmptyEpsilon/#tabs=4. --- Near the far outpost of Orion-5, Exuari attacks are increasing. A diplomat went missing, and your mission will start with recovering him. -- Type: Mission --Create the main ship for the players. player = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Atlantis") player:setPosition(22400, 18200):setCallSign("Epsilon") SpaceStation():setTemplate("Small Station"):setFaction("Human Navy"):setPosition(23500, 16100):setCallSign("Research-1") SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy"):setPosition(-25200, 32200):setCallSign("Orion-5") --Grab a reference to Omega station. enemy_station = SpaceStation():setTemplate("Large Station"):setFaction("Exuari") enemy_station:setPosition(-45600, -15800):setCallSign("Omega") SpaceStation():setTemplate("Small Station"):setFaction("Independent"):setPosition(9100,-35400):setCallSign("Refugee-X") --Nebula that hide the enemy station. Nebula():setPosition(-43300, 2200) Nebula():setPosition(-34000, -700) Nebula():setPosition(-32000,-10000) Nebula():setPosition(-24000,-14300) Nebula():setPosition(-28600,-21900) --Random nebulae in the system. Nebula():setPosition( -8000,-38300) Nebula():setPosition( 24000,-30700) Nebula():setPosition( 42300, 3100) Nebula():setPosition( 49200, 10700) Nebula():setPosition( 3750, 31250) Nebula():setPosition(-39500, 18700) --Create 100 asteroids. for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) VisualAsteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end --Create the defense for the station. CpuShip():setTemplate("Starhammer II"):setFaction("Exuari"):setPosition(-44000, -14000):orderDefendTarget(enemy_station) CpuShip():setTemplate("Phobos T3"):setFaction("Exuari"):setPosition(-47000, -14000):orderDefendTarget(enemy_station) enemy_dreadnought = CpuShip():setTemplate("Atlantis X23"):setFaction("Exuari") enemy_dreadnought:setPosition(-46000, -18000):orderDefendTarget(enemy_station) CpuShip():setTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18100):orderDefendTarget(enemy_dreadnought) CpuShip():setTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18200):orderDefendTarget(enemy_dreadnought) --Small Exuari strike team in the nebula at G5. CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3550, 31250) CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3950, 31250) end function update(delta) --When the player ship is destroyed, call it a victory for the Exuari. if not player:isValid() then victory("Exuari") end --When Omega station is destroyed, call it a victory for the Human Navy. if not enemy_station:isValid() then victory("Human Navy") end end

Ready for your orders?

Now that we know how to create a basic universe and fill it with some enemies, let's start creating a mission.

To kick off this mission, we'll send a transmission from Research-1 to the player, indicating that a transport ship carrying a diplomat was lost en route from Research-1 station to Orion-X.

This involves more code, and we'll explain it in less detail than the previous steps. Keep the Lua reference and script reference handy in case you need help.

Your first order!

Start by creating references for the rest of the stations, as we'll need them later on.

research_station = SpaceStation():setTemplate("Small Station"):setFaction("Human Navy") research_station:setPosition(23500, 16100):setCallSign("Research-1") main_station = SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy") main_station:setPosition(-25200, 32200):setCallSign("Orion-5") enemy_station = SpaceStation():setTemplate("Large Station"):setFaction("Exuari") enemy_station:setPosition(-45600, -15800):setCallSign("Omega") neutral_station = SpaceStation():setTemplate("Small Station"):setFaction("Independent") neutral_station:setPosition(9100,-35400):setCallSign("Refugee-X")

At the end of the init function, send a transmission to the player by using the sendCommsMessage() function. Note how we use double brackets [[ to wrap multiple lines of text.

--Start off the mission by sending a transmission to the player research_station:sendCommsMessage(player, [[Epsilon, please come in? We lost contact with our transport RT-4, who was transporting a diplomat from our research station to Orion-X. Last contact was before RT-4 entered the nebula at G5. Please investigate and recover the diplomat if possible!]])

Improving the enemy strike team

We still have the Exuari strike team set up in the nebula at G5. However, they have roaming orders and will fly out of the nebula to engage enemies. Instead, we want the strike team to hide in the nebula. We also need a transport (designated RT-4, as in our comms message) in the nebula.

By default, friendly ships have a basic comms script (comms_ship.lua in the scripts folder) that responds to player hails and takes basic orders. We don't want RT-4 to respond, so we can set the comms script to nothing ("").

transport_RT4 = CpuShip():setTemplate("Flavia"):setFaction("Human Navy"):setPosition(3750, 31250) transport_RT4:orderIdle():setCallSign("RT-4"):setCommsScript("")

We want the Exuari strike team to destroy RT-4 in front of the players' eyes, so we assign them references and set their orders to Idle so they won't move until ordered. We also set RT-4's hull and shields to 1 so that it can be destroyed in a single shot.

transport_RT4:setHull(1):setShieldsMax(1, 1) --Small Exuari strike team, guarding RT-4 in the nebula at G5. exuari_RT4_guard1 = CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3550, 31250):setRotation(0) exuari_RT4_guard2 = CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3950, 31250):setRotation(180) exuari_RT4_guard1:orderIdle() exuari_RT4_guard2:orderIdle()

Next, we'll script the mission itself. First, set the initial mission state at the end of the init() function. We'll do that with a variable to track the mission's current state, and we'll increase it each time the players trigger a new event.

--Set the initial mission state mission_state = 1

Change the update() function. We still want the mission to end with an Exuari victory when the player is destroyed, and we also want the Exuari to win if Research-1 is destroyed. For now, however, remove the Human Navy victory condition.

function update(delta) --When the player ship or research station is destroyed, call it a victory for the Exuari. if not player:isValid() or not research_station:isValid() then victory("Exuari") end

Now we'll set up what happens during our initial mission state of 1. When the player gets within 5U of transport RT-5, order the Exuari ships to roam around. This causes them to target and destroy RT-4, then target the player ship.

if mission_state == 1 then if distance(player, transport_RT4) < 5000 then exuari_RT4_guard1:orderRoaming() exuari_RT4_guard2:orderRoaming() mission_state = 2 end end end

Finally, outside of the update() function, add a new utility function to measure the distance between two objects. This function uses the getPosition() function to get an object's X and Y coordinates. The local keyword is a Lua feature that prevents these variables from existing outside of this function, which is safer.

function distance(obj1, obj2) local x1, y1 = obj1:getPosition() local x2, y2 = obj2:getPosition() local xd, yd = (x1 - x2), (y1 - y2) return math.sqrt(xd * xd + yd * yd) end

Get the Exuari scum! (Oh, and save the diplomat.)

We can quite safely assume that the Exuari will destroy RT-4 during mission_state 2. In this new state, we'll wait until RT-4 is destroyed to spawn an empty supply drop as an escape pod. We'll also transmit a message to the Epsilon to tell the players what to do. Back in the update() function, add the following:

if mission_state == 2 then if not transport_RT4:isValid() then -- RT-4 destroyed, send a transmission to the player, create a supply drop to indicate an escape pod. mission_state = 3 transport_RT4_drop = SupplyDrop():setFaction("Human Navy"):setPosition(3750, 31250) research_station:sendCommsMessage(player, [[RT-4 has been destroyed! But an escape pod is ejected from the ship. Lifesigns detected in the pod, please pick up the pod to see if the diplomat made it. His death would be a great blow to the peace negotiations in the region. And destroy those Exuari scum while you are at it!]]) end

Once again, we increase mission_state to trigger the next event. We also create the supply drop and keep a reference to it, and we send new instructions to the player ship.

We want the escape pod to be picked up, but we also want the Exuari ships to be destroyed before we continue. After we pick up the escape pod, we want the players to deliver it to the station and finish the mission.

if mission_state == 3 then if not transport_RT4_drop:isValid() and not exuari_RT4_guard1:isValid() and not exuari_RT4_guard2:isValid() then -- Escape pod picked up, all Exuari ships destroyed. mission_state = 4 research_station:sendCommsMessage(player, [[The diplomat is safely picked up! Thanks for that. Please deliver the diplomat to Orion-5 in sector G3. Do this by docking with the station.]]) end end

With the diplomat aboard the Epsilon, we can set up the Human Navy victory condition: his safe delivery to Orion-5.

if mission_state == 4 then if player:isDocked(main_station) then -- Docked and delivered the diplomat. victory("Human Navy") end end

Tips & tricks

There is a LOT you can do with mission scripts, even with the limited tools at your disposal. Mission progression doesn't have to be as linear as in this example, and you could create branching plotlines or randomized conditions. For example, you could make RT-4 survive the assault long enough for the players to have a chance of destroying the Exuari ships first, which could yield more rewards or lead to a different mission path.

If you want to try some things for yourself but aren't sure where to go from here, try setting up a retaliatory Exuari strike after rescuing the diplomat. Spawn some ships near the edge of the nebulae at Omega station and order them to attack the player ship. You can also send a transmission from the new enemy ships to the players to let them know why they are being attacked.

Compiled example

This is the script so far:

-- Name: Beacon of light series -- Description: The beacon of light scenario, build from the scripting tutorial at https://daid.github.io/EmptyEpsilon/#tabs=4. --- Near the far outpost of Orion-5, Exuari attacks are increasing. A diplomat went missing, and your mission will start with recovering him. -- Type: Mission -- Init is run when the scenario is started. Create your initial world function init() --Create the main ship for the players. player = PlayerSpaceship():setFaction("Human Navy"):setTemplate("Atlantis") player:setPosition(22400, 18200):setCallSign("Epsilon") research_station = SpaceStation():setTemplate("Small Station"):setFaction("Human Navy") research_station:setPosition(23500, 16100):setCallSign("Research-1") main_station = SpaceStation():setTemplate("Medium Station"):setFaction("Human Navy") main_station:setPosition(-25200, 32200):setCallSign("Orion-5") enemy_station = SpaceStation():setTemplate("Large Station"):setFaction("Exuari") enemy_station:setPosition(-45600, -15800):setCallSign("Omega") neutral_station = SpaceStation():setTemplate("Small Station"):setFaction("Independent") neutral_station:setPosition(9100,-35400):setCallSign("Refugee-X") --Nebula that hide the enemy station. Nebula():setPosition(-43300, 2200) Nebula():setPosition(-34000, -700) Nebula():setPosition(-32000,-10000) Nebula():setPosition(-24000,-14300) Nebula():setPosition(-28600,-21900) --Random nebulae in the system Nebula():setPosition( -8000,-38300) Nebula():setPosition( 24000,-30700) Nebula():setPosition( 42300, 3100) Nebula():setPosition( 49200, 10700) Nebula():setPosition( 3750, 31250) Nebula():setPosition(-39500, 18700) --Create 100 asteroids. for asteroid_counter=1,50 do Asteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) VisualAsteroid():setPosition(random(-10000, -5000), random(-10000, 30000)) end -- Create the defense for the station CpuShip():setTemplate("Starhammer II"):setFaction("Exuari"):setPosition(-44000, -14000):orderDefendTarget(enemy_station) CpuShip():setTemplate("Phobos T3"):setFaction("Exuari"):setPosition(-47000, -14000):orderDefendTarget(enemy_station) enemy_dreadnought = CpuShip():setTemplate("Atlantis X23"):setFaction("Exuari") enemy_dreadnought:setPosition(-46000, -18000):orderDefendTarget(enemy_station) CpuShip():setTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18100):orderDefendTarget(enemy_dreadnought) CpuShip():setTemplate("MT52 Hornet"):setFaction("Exuari"):setPosition(-46000, -18200):orderDefendTarget(enemy_dreadnought) transport_RT4 = CpuShip():setTemplate("Flavia"):setFaction("Human Navy"):setPosition(3750, 31250) transport_RT4:orderIdle():setCallSign("RT-4"):setCommsScript("") transport_RT4:setHull(1):setShieldsMax(1, 1) --Small Exuari strike team, guarding RT-4 in the nebula at G5. exuari_RT4_guard1 = CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3550, 31250):setRotation(0) exuari_RT4_guard2 = CpuShip():setTemplate("Adder MK5"):setFaction("Exuari"):setPosition(3950, 31250):setRotation(180) exuari_RT4_guard1:orderIdle() exuari_RT4_guard2:orderIdle() --Start off the mission by sending a transmission to the player research_station:sendCommsMessage(player, [[Epsilon, please come in? We lost contact with our transport RT-4, who was transporting a diplomat from our research station to Orion-X. Last contact was before RT-4 entered the nebula at G5. Please investigate and recover the diplomat if possible!]]) --Set the initial mission state mission_state = 1 end function update(delta) --When the player ship or the research station is destroyed, call it a victory for the Exuari. if not player:isValid() or not research_station:isValid() then victory("Exuari") end if mission_state == 1 then if distance(player, transport_RT4) < 5000 then exuari_RT4_guard1:orderRoaming() exuari_RT4_guard2:orderRoaming() mission_state = 2 end end if mission_state == 2 then if not transport_RT4:isValid() then -- RT-4 destroyed, send a transmission to the player, create a supply drop to indicate an escape pod. mission_state = 3 transport_RT4_drop = SupplyDrop():setFaction("Human Navy"):setPosition(3750, 31250) research_station:sendCommsMessage(player, [[RT-4 has been destroyed! But an escape pod is ejected from the ship. Lifesigns detected in the pod, please pick up the pod to see if the diplomat made it. And destroy those Exuari scum while you are at it!]]) end end if mission_state == 3 then if not transport_RT4_drop:isValid() and not exuari_RT4_guard1:isValid() and not exuari_RT4_guard2:isValid() then -- Escape pod picked up, all Exuari ships destroyed. mission_state = 4 research_station:sendCommsMessage(player, [[The diplomat is safely picked up! Thanks for that. Please deliver the diplomat to Orion-5 in sector G3. Do this by docking with the station.]]) end end if mission_state == 4 then if player:isDocked(main_station) then -- Docked and delivered the diplomat. victory("Human Navy") end end end function distance(obj1, obj2) local x1, y1 = obj1:getPosition() local x2, y2 = obj2:getPosition() local xd, yd = (x1 - x2), (y1 - y2) return math.sqrt(xd * xd + yd * yd) end

Download

Downloads for Windows and Android are available at: [download list]

You need to use the same version number for all users or else the game will not work correctly. Mixing different version numbers is not supported.

Source

The sourcecode is up on github at:

https://github.com/daid/EmptyEpsilon (Game source code)

https://github.com/daid/SeriousProton (Engine source code)

Download [PRERELEASE]

Downloads for Windows and Android are available at: [download list]

You need to use the same version number for all users or else the game will not work correctly. Mixing different version numbers is not supported.

Source

The sourcecode is up on github at:

https://github.com/daid/EmptyEpsilon (Game source code)

https://github.com/daid/SeriousProton (Engine source code)

Main Discord

Come and talk to the developers and players of EmptyEpsilon on the USN discord.

Hosted by the USN, who handle both Artemis and EmptyEpsilon games.

Regular EmptyEpsilon games are hosted on the USN discord. See the agenda for planned games.

Forums

The forums where we communicate about the game can be found at: bridgesim.net.

Other

Unofficial EmptyEpsilon Discord: https://discord.gg/buRcFfm

Issues

You can report issues on github at https://github.com/daid/EmptyEpsilon/issues. Remember to stay civil, and that the maintainers have no obligation to you.

(Want your link on this page? Feel free to let us know on a github issue, and we can update this list)

Find a local game

Quantum Games Belgian location. French language. Offers customized experiences.

Things using EmptyEpsilon

Odysseus was a Finnish style larp for 104 players set in a world inspired by Battlestar Galactica. The larp was played in Helsinki in summer 2019.