tic80playground

Tic80 playground

View on GitHub

Demo Engine

This demo engine was used in the Beach Relaxation demo. The demo engine provides a system that switches between different parts written by the author of the demo. Different parts which can be switched when they are finished or after a time delay. Each part consists of effects which are shown at the same time. The engine presented here is a variant which will be improved over time.

It is separated into different include files, all named “….inc.lua” which can be included into your demo, use the preprocessor of your choice or the one in the T7D’s Docker Image. All examples in this manual will use the Docker image. Or just keep everything in a single file and use copy-and-paste to put the code into your demo.

Class System

The file “class.inc.lua” contains a base class and some convenience classes to used in the engine.

Class

The base class “Class” has two methods: “extend” and “new”.

Use “extend” to derive a new class from the base class. Multiple inheritance is currently not supported. Use like this:

Myclass = Class:extend{data = "World"}
funcion Myclass:hello()
   print("Hello " .. self.data)
end

Use “new” to create a new object. Using the above class:

myobj = Myclass:new{}
myobj:hello()

This would print “Hello World”.

Engine

The engine consists of some constants, convenience function, and a demo class. Effects are the core of the demo, grouped together in parts. As parts may keep the current effects running, parts is used as a loosly term here.

Constants

Constants for the flow control are:

Palettes:

Convenience functions

mkBackground(col)

This function returns a function which clears the background with the given colour “col”. It is best used at the beginning of a part to clear the screen:

...
parts = {
	{
		code = {
			mkBackground(0), -- Black background.
			... -- Some more effects
		}
	}
}

🗈 Note, if the mkBackground() call would be replaced with cls(0) this would not have the desired effect as the screen would only be cleared once when the table is build and the return value (nil) would be put into the list of effects. This would be an error when running the demo.

delay_frames(frames, effect)

After “frames” frames the effect is moved into the list of effects.

delay_ms(ms, effect)

After “ms” milliseconds the effect is moved into the list of effects.

wait_signal(signal_name, effect)

This function waits for the signal with the signal name “signal_name” and if this signal is received the effect replaces the waiting funciton.

Engine class

The demo engine class should be instanciated in the BOOT() function and set some global variable, probably “demo”. We will use “demo” as our engine object throughout this documentation. Every frame in the TIC() function a call to demo:run() should be made.

When creating the demo object pass the demo parts in the table to the new() call as “parts”. If needed for debugging then setting “partidx” to the number of part one would like to start with minus one.

The bare minimum, which does acually nothing, demo code is:

--|#include "t7d/class.inc.lua"
--|#include "t7d/engine.inc.lua"

function BOOT()
   demo = Engine:new{
      parts = {
		  {
			  code = {
			  }
		  }
      }
   }
end

function TIC()
   demo:run()
end

Information on how to build and run your first demo are provided in Usage section.

The “parts” entry in the table given when initialising the Engine is a table with parts which are played one after the other. Each part in turn is a table with the following elements:

When using the duration keep in mind that the check if the duration is used up is only done once per frame with a ≥ check to the current program clock, therefore the granularity is one frame (1/60s of a second).

The code entries in the table are either

and are executed in the order in which they are given in the code table. This can be used to generate layers of effects.

⚠ Due the peculiarities how Lua handles tables be aware that due to the call by reference nature a table initialisation in a class definition may not give the desired result. It will be shared by all instances! ⚠

Effects

Effects are defined by the user of the engine. Each effect is called with a single argument, a list to so-called signals. The signals is a table stored in the game engine and can be used to transport information between effects. Usually it would contain just keys “name” in the table which are set to true to signal that the signale with name “name” is set. This is used to start actions in other effects.

The effect should return either:

Optionally the effect may return a table of new effects which are appended to the list of effects after the current effect but before the other effects not yet executed.

Compression

tbd.

Usage

Two possible usage option are available, one is include everything into a huge file and write your code there or use the includes provided by the Docker image. In the latter case the demo must be build before it can be load in the Tic-80 virtual console.

In this manual we will use the build option as this keeps the code size small. Use whatever is more convenient fo you.

Build

The build process described here uses the Docker Image to create a composited Lua file. In this image the necessary tools and include files are available.

When a file “minimum.lua” should be converted into a file loadable by Tic-80 then use

docker run -u 1000:10 --rm -v $PWD:/host -w /host vintagecomputingcarinthia/tic80build gpp -U "" "" "(" "," ")" "(" ")" "#" "\\" -M '--|#' "\n" " " " " "\n" "(" ")" minimum.lua > cooked.lua
#If using Docker on Windows, replace "$PWD" by "%cd%" but leave the forward slashes!

to build your executable Lua file. Then start Tic-80 in the directory the “cooked.lua” file is with tic80 --fs=. --skip. Then load it (load cooked.lua) and run it.

Glue everything together

Copy the content of class.inc.lua and engine.inc.lua into one file, call it whatever you like, e.g. “demo.lua”, and add the BOOT() and TIC() functions. In Tic-80 use load demo.lua (or the name you gave your demo) to load the file and then run to start your demo.

TODO: Add instructions to convert into a “.tic” file.

Examples

Engine

Call function

This example with a function call just has a single function call to the clear screen function. As this is a static function this is a prudent choice. Excerpt:

function BOOT()
   demo = Engine:new{
      parts = {
         {
            code = {
               function()
                  cls(7)
               end
            }
         }
      }
   }
end

When a dynamic approach is needed then using closures is a convenient way to implement this. We will implement a “blind” closing the screen from left to right. Excerpt:

function mkBlinds()
   local width = 0
   return function()
      rect(0, 0, width, 136, 11)
      if width < 240 then
         width = width + 1
      end
   end
end

function BOOT()
   demo = Engine:new{
      parts = {
         {
            code = {
               function()
                  cls(7)
               end,
               mkBlinds()
            }
         }
      }
   }
end

The first part has two effects, the first effect clears the screen with a dark green colour. The second effect is a function returned from mkblinds() and uses the closure to access the local variable width, which is incremented on each call until the whole screen is covered.

In the “Beach Relaxation” demo the exploding text was implemented using pixels flying off in every direction. The mkPixel() function uses closures to keep track of the pixel position and velocity. The closures accessed are the function parameters passed to mkPixel() which allows to generated many pixels. When the pixel is off screen then the value REMOVE is returned which removes the function from the list of effects.

-- Example from Beach Relaxation.
function mkPixel(x,y,dx,dy,col,sig)
	return function()
		pix(x,y,col)
		x=x+dx
		y=y+dy
		if x<0 or x>=240 or y<0 or y>=136 then
			return REMOVE
		end
	end
end

Object

A class derived from the engine base class can be used as an effect, it needs to implement the run() method which will be called every frame. Actually any table with a run() method will do.

Example class for Lissajous figures:

Lissajous = Class:extend{phi1 = 4e-2, phi2 = 2e-2, omega = 0, tau1 = 0, tau2 = 0}
function Lissajous:run()
   local x = 120 + 50 * math.cos(self.phi1 *  self.omega + self.tau1)
   local y = 68 + 50 * math.sin(self.phi2 * self.omega + self.tau2)
   circ(x, y, 5, 2)
   self.omega = self.omega + 1
end

The Lissajous class derives from the class and implements the run() method. It is instanciated as one of the effects in the BOOT() function.

function BOOT()
   demo = Engine:new{
      parts = {
         {
            code = {
               function()
                  cls(8)
               end,
               Lissajous:new{tau1 = math.pi / 2}
            },
            name = "Lissajous class"
         }
      }
   }
end

Coroutine

Using coroutines is a convenient way to generate functions which can be called repeatedly. In the BOOT() function

function BOOT()
   demo = Engine:new{
      parts = {
         {
            code = {
               function()
                  cls(8)
               end,
               coroutine.wrap(moving_circle)
            },
            name = "coroutine"
         }
      }
   }
end

one part is defined with two effects. The first effect cleans the screen in a dark blue colour, the second is a coroutine which moves a circle around the screen. The while loop is an endless loop and after each circle drawing a call to coroutine.yield() to leave the function and at the next frame the coroutine is resumed.

function moving_circle()
   local phi = 0
   while true do
      local x = 120 + 50 * math.cos(phi)
      local y = 68 + 50 * math.sin(phi)
      circ(x, y, 5, 2)
      phi = phi + 4e-2
      coroutine.yield()
   end
end

The angle φ is incremented at each frame and a circle of a radius of five pixels is drawn moving on the screen.

An alternative option is to use the Coclass class to call a coroutine at each frame. The class is defined as follows:

Coclass = Coroutine:extend{phi = 0}
function Coclass:coroutine()
   while true do
      local x = 120 + 50 * math.cos(self.phi)
      local y = 68 + 50 * math.sin(self.phi)
      circ(x, y, 5, 2)
      self.phi = self.phi + 4e-2
      coroutine.yield()
   end
end

The class definition extends the Coroutine and implements the coroutine() method. The framework calls the run() method at each frame and the the Coroutine class resumes the coroutine() method in turn. If a more sophisticated approach for an effect is needed this is a possible way to implement it.

Spawning effects

The pixel spawning example shows how to spawn effects. The effects in this part consist of clearing the screen with black mkBackground(0), please note that cls(0) will not work. The next effect is delay by 2s and displays a scroller which will appear behind the pixels as it is drawn earlier (remember that effects are executed in order).

Then the coroutine wraps the following function

function()
	while true do
		local col = math.random(9, 13)
		for counter = 1, 400 do
			if math.random() < .8 then
				local phi = math.random(0, 359)
				local r = math.random() + .2
				local x = r * math.cos(phi / 360 * math.pi * 2)
				local y = r * math.sin(phi / 360 * math.pi * 2)
				coroutine.yield(CONTINUE, { mkPixel(120, 68, x, y, col) })
			end
		end
		for i = 1, 90 do coroutine.yield(CONTINUE) end
	end
end

and is used for creating/spawning moving pixel. In an endless loop a random colour is selected and for 400 frames a moving pixel is spawned with a probability of 80%. In the line with coroutine.yield(CONTINUE, { mkPixel(120, 68, x, y, col) }) the function yields and returns two values: the first value is “CONTINUE” which signals to the engine that the coroutine should continue and be called next frame, the second return value is a table of newly generated effects. The generated effect uses mkPixel() to generated a single moving pixel which, in turn, is a closure that draws and moves the pixel. Once the pixel leaves the screen it will be removed from the list of effects.

After 400 frames of spawning pixels, 90 frames will do nothing to create simulate waves of pixels. For the delay coroutine.yield() could have been used as this returns nothing (nil) and this is interpreted by the engine as “continue effect”.

The last effect is delayed 2.5s and displays a scroller, again. But as this scroller is drawn later, it appear before the moving pixels.

Effects

Scroller

A scroller example shows how to use the scroller class. A simple left-to-right scroller is available in the ltor-scroller.inc.lua include file. In the example two effects are running, the first effect clears the screen with white. The second effect is the scroller object. Object instantiating:

LtoRScroller:new{
	foreground = 2,
	text = "One time scroller text..."
}

The scroller text is set (necessary data member) and the foreground is set to red. Once the scroller is completed the object is removed from the list of effects (this is configurable, though, so other values may be returned).