Skip to content

Commit

Permalink
feat(Screens): Add a way to easily create empty screens (#114)
Browse files Browse the repository at this point in the history
* feat(Screens): Add a way to easily create empty screens

(see new docs)

* fix(screens): small issue with new screens config
  • Loading branch information
liana-p committed Jul 4, 2023
1 parent ff9732a commit 3f5488d
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 81 deletions.
4 changes: 4 additions & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ const config = {
{ text: 'Quests', link: '/features/quests' },
{ text: 'Save and Load', link: '/features/save-and-load' },
{ text: 'Skills', link: '/features/skills' },
{
text: 'Sprites and text',
link: '/features/dynamic-sprites-text-objects.md',
},
{ text: 'Transitions', link: '/features/transitions' },
{ text: 'Viewport', link: '/features/viewport' },
],
Expand Down
10 changes: 1 addition & 9 deletions docs/commands/all-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,7 @@ Imagine $data.myArray contains an array with [25, 50, 75]

## Screen Objects

Screen objects are new and not documented yet, basic usage is to put the result of `create_sprite` or `create_object` in a variable and then manipulate it. Example:

```narrat
set data.playerSprite (create_sprite img/player.png 50 50)
wait 1000
set data.playerSprite.x 100 // moves the player to x 100
wait 1000
delete_sprite $data.playerSprite
```
See [Dynamic sprites and text documentation](../features/dynamic-sprites-text-objects.md) for info on how to use screen objects.

| Command | Example | Description |
| ------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
Expand Down
6 changes: 5 additions & 1 deletion docs/commands/left-side-viewport-commands/set-screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Example:
set_screen map
```

### Layers
## Layers

Screens can use layers, to make it possible to overlay screens on top of each other. By passing a number as the second parameter to `set_screen`, it will set a screen on this layer. Example:

Expand All @@ -25,3 +25,7 @@ main:
```

See [Screens guide](../../features/viewport.md) for more info

### Placeholder screens

Sometimes you might want placeholder screens that are empty and don't need a background image. See [Empty screens](../../features/viewport.md#empty-screens) for more info.
82 changes: 82 additions & 0 deletions docs/features/dynamic-sprites-text-objects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: Dynamic sprites, text and objects in viewport
description: Narrat games can have dynamic sprites and text created by scripts during the game which appear in the viewport.
---

# Screen Objects

Screen objects are dynamic sprites and texts which are not defined in the config but instead created dynamically in sprite.

This enables games to place custom interactive elements on the screen programmatically, without requiring every possible element to have been configured previously.

## How it works

While screens and buttons are static (defined in the config once and can't change), screen objects (sprites and texts) are created dynamically and can be updated.

::: info
Screen objects are rendered as HTML divs, similarly to buttons, but narrat scripting has no way to make them smoothly move for "action" 2D gameplay. If you want to do some 2D game programming with real-time or complex graphics, you should consider using a 2D game engine like in the [narrat-2d](https://github.com/liana-p/narrat-engine/tree/main/packages/narrat-2d) plugin.
:::

In any narrat script, you can create a sprite:

```narrat
main:
set data.my_sprite (create_sprite img/sprites/my_sprite.png 150 150) // Creates a sprite using my_sprite.png at position 150, 150
set data.my_text (create_object 50 50 $data.my_sprite) // Creates a screen object at position 50,50, as a child of the sprite. Position is relative to the parent
set data.my_text.text "Hello world!" // Give a text to the object
jump move_sprite
move_sprite:
choice:
"Where should I move the sprite?":
"Left":
add data.my_sprite.x -100
"Right":
add data.my_sprite.x 100
"Up":
add data.my_sprite.y -100
"Down":
add data.my_sprite.y 100
```

## Examples

::: tip
The [RPG example](https://github.com/liana-p/narrat-engine/tree/main/packages/narrat/src/examples/rpg/scripts) uses sprites.

The [Spoon Survival](https://github.com/liana-p/narrat-examples/tree/main/spoon-survival) example game also uses sprites.
:::

## Scene graph

Screen objects are rendered in a scene graph, where elements can have children, and every element has a parent (except the top-level ones).

Whenever creating a screen object, passing another screen object as the last parameter will make it the parent, as in the example above.

## Options

Screen objects are just [objects](../scripting/language-syntax.md#objects) that contain properties that define how they should be displayed.

You can simply set properties of the variable that the `create_sprite` or `create_object` function returned to modify the object, like in the example above.

Available properties to change (all of them are optional, the only mandatory ones are the ones passed to `create_sprite` or `create_object`, as above):

- `name`: A name, not really used yet but useful to tell sprites apart for debugging
- `x`: The x position of the object in pixels
- `y`: The y position of the object in pixels
- `anchor`: An object with x and y properties for the anchor of the object between 0 and 1. 0 means rendered from left corner, 1 means from right corner. An anchor of 0.5,0.5 would be rendering from the center
- `x`: The x anchor of the object
- `y`: The y anchor of the object
- `width`: The width of the object in pixels. **Note: For sprites this will be set automatically to the size of the image**
- `height`: The height of the object in pixels. **Note: For sprites this will be set automatically to the size of the image**
- `opacity`: Opacity between 0 and 1
- `scale`: The width and height of the object will be multiplied by this number if present
- `layer`: Which viewport layer to render the sprite on. If on layer 1, the sprite will render in front of the screen on layer 1, but behind the screen in layer 2. **Note: If a layer has no active screen, sprites won't be rendered there. If needed you can create [empty placeholder screens](../features/viewport.md#empty-screens) so that sprites on a layer are rendered**.
- `cssClass`: An optional CSS class name to give to the sprite. This allows you to apply custom CSS styling to any sprites
- `onClick`: A label to run when the sprite is clicked. Example: `set data.mySprite.onClick my_label`, or if you want to pass arguments: `set data.mySprite.onClick clicked_sprite my_argument`.

## Difference between sprite and object

Sprites are screen objects that also render an image. Normal screen objects contain nothing by default and are just an empty div.

Adding a text to a screen object adds a text inside the div.
19 changes: 17 additions & 2 deletions docs/features/viewport.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ description: >-
Narrat games can have different screens which are 2D images used to illustrate
the story. Screens are also interactive and can have buttons to click on
elementsNarrat games can have different screens
cover: ../.gitbook/assets/image (31).png
coverY: 0
---

# Screens
Expand Down Expand Up @@ -105,6 +103,23 @@ This script would be triggered by pressing the `parkButton` in the `map` screen
The `default` screen must always exist, as it is the first screen the game gets loaded with.
:::

## Empty screens

Sometimes, you might need an empty placeholder screen on a layer that has no background. To avoid needing to create a placeholder background, there is a shortcut.

Setting the screen to a screen called `@empty`, or setting the background of any screen in the config to `@empty`, will make the screen have no background. This way you don't need an asset for the placeholder layer

This is useful if you want multiple layers of sprites but don't need screens for all of them. You can just create set empty screens for those layers.

Example:

```narrat
main:
set_screen @empty 2
var sprite (create_sprite img/my_sprite.png 150 150)
set sprite.layer 2 // The sprite can appear because there is a placeholder empty screen on layer 2
```

## Button interaction tags

Buttons can also have a `tag` property in their config to use interaction tag, the same way inventory items can. See the guide below for more info on interaction tags (in the interaction tags section):
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ features:
- icon: 👾
title: Sprites system for advanced use
details: A sprite system allows dynamic creation of sprites and text with a scene graph to dynamically create custom elements, UI, etc.
link: /commands/all-commands#screen-objects
link: /features/dynamic-sprites-text-objects
linkText: Screen Objects docs
- icon: ⚙️
title: Powerful plugin API
Expand Down
9 changes: 7 additions & 2 deletions packages/narrat/src/components/saves/save-slot-ui.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ import { SaveSlot } from '../../types/game-save';
import { computed, PropType, ref } from 'vue';
import { toHHMMSS } from '../../utils/time-helpers';
import { renameSave } from '../../utils/save-helpers';
import { getConfig, getImageUrl, screensConfig } from '@/config';
import {
getConfig,
getImageUrl,
getScreenConfig,
screensConfig,
} from '@/config';
const props = defineProps({
saveSlot: {
Expand All @@ -70,7 +75,7 @@ const saveScreenshot = computed(() => {
const save = props.saveSlot.saveData;
if (save) {
if (save.screen.layers[0]) {
const conf = screensConfig().screens[save.screen.layers[0]];
const conf = getScreenConfig(save.screen.layers[0]);
if (conf) {
return {
backgroundImage: `url(${getImageUrl(conf.background)})`,
Expand Down
65 changes: 10 additions & 55 deletions packages/narrat/src/components/screen-layer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
getConfig,
getButtonConfig,
getImageUrl,
screensConfig,
getScreenConfig,
} from '@/config';
import { computed, CSSProperties } from 'vue';
import { useMain } from '../stores/main-store';
Expand All @@ -44,6 +44,7 @@ import { audioEvent } from '@/utils/audio-loader';
import { error } from '@/utils/error-handling';
import ScreenObject from './screen-objects/screen-object.vue';
import { isViewportElementClickable } from '@/utils/viewport-utils';
import { EMPTY_SCREEN } from '@/constants';
const props = defineProps<{
layer: string;
Expand Down Expand Up @@ -72,7 +73,7 @@ const buttonsState = computed(() => {
});
const screenConfig = computed(() => {
const conf = screensConfig().screens[currentScreen.value];
const conf = getScreenConfig(currentScreen.value);
if (!conf) {
console.log(currentScreen);
error(`Screen ${currentScreen.value} doesn't have a config`);
Expand Down Expand Up @@ -193,64 +194,18 @@ function getButtonText(button: string): string {
}
const layerStyle = computed<CSSProperties>(() => {
let backgroundImage: string | undefined = `url(${getImageUrl(
screenConfig.value.background,
)})`;
if (screenConfig.value.background === EMPTY_SCREEN) {
backgroundImage = undefined;
}
return {
backgroundImage: `url(${getImageUrl(screenConfig.value.background)})`,
backgroundImage,
width: `${layoutWidth.value}px`,
height: `${layoutHeight.value}px`,
};
});
// Sprites
function clickOnSprite(sprite: SpriteState) {
if (props.transitioning) {
return;
}
if (sprite.onClick) {
useScreenObjects().clickObject(sprite);
}
}
function getSpriteClass(sprite: SpriteState): { [key: string]: boolean } {
const css: any = {};
if (sprite.onClick) {
css.interactable = true;
} else {
css.disabled = true;
}
if (sprite.cssClass) {
css[sprite.cssClass] = true;
}
return css;
}
function getSpriteStyle(sprite: SpriteState): CSSProperties {
const style: CSSProperties = {};
if (sprite.opacity !== 1) {
style.opacity = sprite.opacity;
}
let left = sprite.x;
let top = sprite.y;
if (sprite.anchor) {
const anchor = sprite.anchor;
left = sprite.x - sprite.width * anchor.x;
top = sprite.y - sprite.height * anchor.y;
style.transformOrigin = `${anchor.x * 100}% ${anchor.y * 100}%`;
}
let width = sprite.width;
let height = sprite.height;
if (sprite.scale) {
width = width * sprite.scale;
height = height * sprite.scale;
}
return {
...style,
left: `${left}px`,
top: `${top}px`,
backgroundImage: `url(${getImageUrl(sprite.image)})`,
width: `${width}px`,
height: `${height}px`,
};
}
</script>
<style>
.viewport {
Expand Down
25 changes: 16 additions & 9 deletions packages/narrat/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { AppOptions } from './types/app-types';
import { error } from './utils/error-handling';
import { useConfig } from './stores/config-store';
import { Config, defaultConfig } from './config/config-output';
import { DEFAULT_DIALOG_WIDTH } from './constants';
import {
DEFAULT_DIALOG_WIDTH,
EMPTY_SCREEN,
defaultScreenConfig,
} from './constants';
import {
defaultItemsConfig,
ItemsInputConfigSchema,
Expand Down Expand Up @@ -209,6 +213,17 @@ export function charactersConfig() {
return getConfig().characters;
}

export function getScreenConfig(screen: string): ScreenConfig {
if (screen === EMPTY_SCREEN) {
return defaultScreenConfig;
}
if (!screensConfig().screens[screen]) {
error(`Screen config for screen ${screen} doesn't exist`);
return defaultScreenConfig;
}
return screensConfig().screens[screen];
}

export function getTooltipConfig(keyword: string) {
const config = tooltipsConfig();
const data = config.tooltips.find((k) => k.keywords.includes(keyword));
Expand Down Expand Up @@ -280,14 +295,6 @@ export function getButtonConfig(button: string): ButtonConfig {
return result;
}

export function getScreenConfig(screen: string): ScreenConfig {
const result = screensConfig().screens[screen];
if (!result) {
error(`Screen config for screen ${screen} doesn't exist`);
}
return result;
}

export function getItemConfig(id: string) {
const item = itemsConfig().items[id];
if (!item) {
Expand Down
6 changes: 6 additions & 0 deletions packages/narrat/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ScreenConfig } from './config/screens-config';

export const PRE_SIGNAL = '###_--_~=:;';
export const JUMP_SIGNAL = `###_--_~=:;_JUMP`;
export const RETURN_SIGNAL = `###_--_~=:;_RETURN`;
Expand All @@ -23,3 +25,7 @@ export const BUILD_DATE = new Date(import.meta.env.VITE_BUILD_DATE);
// Default values
export const DEFAULT_DIALOG_WIDTH = 400;
export const DEFAULT_TEXT_SPEED = 20;
export const EMPTY_SCREEN = '@empty';
export const defaultScreenConfig: ScreenConfig = {
background: EMPTY_SCREEN,
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ main:
// unlock_achievement win_game
// talk player idle "Global counter is %{$global.counter}"
// jump test_hmr
set_screen @empty 2
jump quest_demo

test_hmr:
Expand Down
8 changes: 6 additions & 2 deletions packages/narrat/src/stores/screen-objects-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,18 @@ export const useScreenObjects = defineStore('screenObjects', {
if (index !== -1) {
parent.children.splice(index, 1);
} else {
warning(`Could not find object ${object.id} in parent's children`);
// console.warning(`Could not find object ${object.id} in parent's children`);
console.warn(
`Could not find object ${object.id} in parent's children`,
);
}
} else {
const index = this.tree.indexOf(object);
if (index !== -1) {
this.tree.splice(index, 1);
} else {
warning(`Object to destroy not found in store (${object.id})`);
// warning(`Object to destroy not found in store (${object.id})`);
console.warn(`Object to destroy not found in store (${object.id})`);
}
}
delete this.objectsList[object.id];
Expand Down

0 comments on commit 3f5488d

Please sign in to comment.