Cutscenes - Hirato/lamiae GitHub Wiki

Lamiae uses a relatively generic system for defining cutscenes. There are three very important things to note when cutscenes are active.

  1. The player can be controlled via AI directives
  2. The commands mainly manipulate the camera
  3. The regular HUD is not drawn, the cutscene subsystem has its own with its own rules.

Basic Anatomy of a Cutscene

ICOMMAND(cs_start, "s", (const char *s),
	...
	formatstring(file)("data/rpg/games/%s/cutscenes/%s.cfg", game::data, s);
	exefile(file);
	...
)

ICOMMAND(cs_post, "e", (uint *body),

The only thing you need for a cutscene is a respective configuration file within your game's "cutscenes" directory. This file can have any name, provided they are legal filenames. Provided the cutscene.cfg file exists within the cutscenes directory, it can be invoked simply by cs_start my_cutscene

Another very important thing to remember with cutscenes is that they can always be skipped. This is where cs_post comes in. cs_post is a command which takes a script body, which is executed upon termination of the cutscene. If you need to guarantee that certain characters or events occur at the end of a cutscene, use this.

cs_post [
	// For example, if we wanted the player to be dead after this cutscene...
	r_kill player
]

Cutscenes can be interrupted in several ways.

  1. There are no cutscene elements left
  2. A map/game event interrupts it
  3. The user invokes cs_interrupt

Entities

See Entities for a full guide on the available entity types.

camera:
	attr1: tag
	attr2: yaw
	attr3: pitch
	attr4: roll

The camera entity is only useful in cutscenes. It is used both to mark locations and possible camera configurations. This information is used by cs_action_viewcamera and cs_action_movecamera to manipulate the camera.

Anatomy of the Elements

struct action
{
	int startmillis, duration;
	uint *successors;
	float (*interp)(int, int, bool);

	action(int d, uint *s)
	virtual ~action()

	virtual bool update()
	virtual void render(int w, int h) {}
	virtual void debug(int &hoffset, bool type)
	virtual void getsignal(const char *sig)
};

There are several types of elements and they each have their unique quirks, and they all inherit from the above prototype. The basics of each and everyone of them is that their actions are performed over a set duration as defined by an interpolation function. When they die, they spawn a series of children, or successors.

Interpolation Functions

static float function(int startmillis, int duration, bool delta)

All Interpolation functions are of the above form, and they define the gradient of travel from 0 --> 1, or 1 --> 0 as the case may be. The following functions are available, and the call of the format is as follows. Children inherit the interpolation function of their parents unless a different one is specified.

cs_interp_fun [
	cs_action param1 param2 param3 param4 ...
]

Essentially, their arguments are spawned using them as their interpolation function. The following functions are currently available. If you want something to fade in then out, you will need to recreate the drawn element with the opposite interpolation function.

  1. cs_inter_linear
  2. cs_interp_dlinear - Descending Linear
  3. cs_interp_sin - goes 0 --> 1
  4. cs_interp_sin2 - Above, but squared
  5. cs_interp_cos - goes 1 --> 0
  6. cs_interp_cos2 - Above, but squared
  7. cs_interp_one - instantly completes actions

Generic

Delay/Sleep

This uses the action structure and doesn't add any additional fields. It takes the delay in milliseconds, like all other commands.

prototype:
	ICOMMAND(cs_action_generic, "ie", (int *d, uint *s),
script:
	cs_action_generic 5000 [
		cs_action_moveent ...
	]

Signal

struct signal : action
{
	const char *name;
	bool received;

	...
};

Signals are semi-permanent fixtures, in the sense that they never time out, the duration field is irrelevant to them. They will wait, and wait, until a signal is sent from the game in a way that allows the cutscene system access to it.

prototype:
	ICOMMAND(cs_action_signal, "se", (const char *sig, uint *s),
script:
	cs_action_signal "my signal" [
		cs_action_generic 5000 ...
	]

Camera Actions

Move

struct move : action
{
	float dx, dy, dz;

	...
};

The move action takes 3 extra parameters, describing the delta of the movement it'll be taking over its duration. values of (0, 0, 500) for example means that it will move 500 units upward over its lifetime.

prototype:
	ICOMMAND(cs_action_movedelta, "fffie", (float *x, float *y, float *z, int *d, uint *s),
	ICOMMAND(cs_action_moveset, "fffie", (float *x, float *y, float *z, int
	ICOMMAND(cs_action_movecamera, "iie", (int *tag, int *d, uint *s),
	ICOMMAND(cs_action_moveent, "sie", (const char *r, int *d, uint *s),

There are 4 variations of the command which will create a move action for the camera. The following example will makes the following assumptions, keep them in mind. They will all try to move the camera to the point, (10, 10 10) over 5 seconds

  1. The camera is currently at (50, 40, 30)

  2. The player is at (10, 10, 10)

  3. There is a camera entity with tag 1 at (10, 10, 10)

    script: cs_action_movedelta -40 -30 -20 5000 [ echo done! ] cs_action_moveset 10 10 10 5000 [ echo done! ] cs_action_movecamera 1 5000 [ echo done! ] cs_action_moveent player 5000 [ echo done! ]

View

struct view : action
{
	float dyaw, dpitch, droll;

	...
};

The view action takes 3 extra parameters, describing the deltas to apply to the camera's yaw, pitch and roll over its duration. To do a barrel roll, the values (0, 0, 360) would be used.

prototype:
	ICOMMAND(cs_action_viewdelta, "fffie", (float *y, float *p, float *r, int *d, uint *s),
	ICOMMAND(cs_action_viewset, "fffie", (float *y, float *p, float *r, int *d, uint *s),
	ICOMMAND(cs_action_viewcamera, "iie", (int *tag, int *d, uint *s),
	ICOMMAND(cs_action_viewent, "sie", (const char *r, int *d, uint *s),

Like the Move action, there are 4 variations which take delta, set, and reference values to compute the strength of the action. Again, we will make some assumptions and try to end up with the values, 90, 0, 0.

  1. The camera is currently oriented as (45, 30, 15)

  2. The player and a camera entity of tag 1 have the yaw, pitch, and roll values, (90, 0, 0), ascribed to them.

    script: cs_action_viewdelta 45 -30 -15 5000 [ echo done! ] cs_action_viewset 90 0 0 5000 [ echo done! ] cs_action_viewcamera 1 5000 [ echo done! ] cs_action_viewent player 5000 [ echo done! ]

Focus

struct focus : action
{
	dynent *ent;
	float interp, lead;

	...
};

prototype:
	ICOMMAND(cs_action_focus, "sffie", (const char *r, float *i, float *l, int *d, uint *s),

Follow

struct follow : action
{
	dynent *ent;
	float interp, tail, height;

	...
};

prototype:
	ICOMMAND(cs_action_follow, "sfffie", (const char *r, float *i, float *t, float *h, int *d, uint *s),

Viewport

struct viewport : action
{
	dynent *ent;

	...
};

The Viewport action, literally looks through a reference entity's viewport. This basically forces the reference object's location, and his yaw/pitch/roll values onto the camera. As a result, the other actions are temporarily voided.

prototype:
	ICOMMAND(cs_action_viewport, "sie", (const char *r, int *d, uint *s),

script:
	cs_action_viewport enemy:0 5000

Containers

struct container : action
{
	vector<action *> pending;
	...
};

An interesting note with the containers, is that despite taking a "duration field," they do not expire until after their children have expired. As such, containers are very useful for grouping command actions and elements together. Especially for managing the latter in layers.

Some containers also apply transformations, such as translation, the duration in this case is used to define the time over which the transformation will take place, but as previously mentioned, even once the transformation is finalised, they will not expire until their children have expired.

This makes it really useful for splitting actions and elements into layers, the latter especially as they have a visual focus. And you could use these to do fades and add narrated slides without having to worry that they'd obscure the subtitles!

For example, let's say you had credits rolling past at the climax of your game, and in the background you're cycling through various bits and pieces of concept art behind the credits. You could group things up like so...

cs_element_solid ... //draw a background
cs_container_generic [
	//add concept art here in a sequential manner
]
cs_container_translate 0 -5000 [
	//long list of credits here
] 90000

Such a setup allows you to easily split the whole thing into manageable layers, some of which just contain a bunch of children, others which apply constant transformations onto their children.

Generic

prototype:
	ICOMMAND(cs_container_generic, "eie", (uint *c, int *d, uint *s),

Rotate

struct rotate : container
{
	float angle;

	...
};

The rotation transform rotates its contents by the specified amount in degrees over its lifetime.

prototype:
	ICOMMAND(cs_container_rotate, "feie", (float *a, uint *c, int *d, uint *s),
script:
	cs_container_rotate 90 [ ... ] 5000

Scale

struct scale : container
{
	float dx, dy;

	...
};

prototype:
	ICOMMAND(cs_container_scale, "ffeie", (float *x, float *y, uint *c, int *d, uint *s),

Translate

struct translate : container
{
	float dx, dy;

	...
};

prototype:
	ICOMMAND(cs_container_translate, "ffeie", (float *x, float *y, uint *c, int *d, uint *s),

On-Screen Elements

Like the HUD system, a 1600x1200 virtual screen is scaled to fit your viewport. The boundaries of which are then extended to cover the whole screen. These boundaries are available via the variables, hud_right and hud_bottom, the top left is the origin point, (0, 0).

Text

struct text : action
{
	float x, y, width;
	int colour;
	const char *str;

	...
};
prototype:
	ICOMMAND(cs_element_text, "fffisie", (float *x, float *y, float *w, int *c, const char *t, int *d, uint *s),

Image

struct image : action
{
	float x, y, dx, dy;
	int colour;
	const char *img;

	...
};

prototype:
	ICOMMAND(cs_element_image, "ffffisie", (float *x, float *y, float *dx, float *dy, int *c, const char *p, int *d, uint *s),

Solid

struct solid : action
{
	float x, y, dx, dy;
	int colour;
	bool modulate;

	...
};

prototype:
	ICOMMAND(cs_element_solid, "ffffiiie", (float *x, float *y, float *dx, float *dy, int *c, int *m, int *d, uint *s),
⚠️ **GitHub.com Fallback** ⚠️