LogoPixi’VN
Crea tu primer proyecto

Make your first Point & Click Adventure

Before continuing, it is important to consider that this page assumes the concepts explained in "Make your first Visual Novel", and that all the features explained on that page can also be applied here. Additionally, having an intermediate knowledge of JS/TS (such as functions, classes, interfaces, and the use of .d.ts files) is essential. Do not continue if you do not have this knowledge.

This tutorial will guide you through the process of creating your first Point & Click Adventure.

What is a Point & Click Adventure? A Point & Click Adventure is a game genre in which the player interacts with the environment and objects through mouse pointing and clicking. These games often feature puzzles, dialogues, and an engaging narrative.

How to implement it in a Pixi’VN project? The Pixi’VN environment provides a utility library and base models, called NQTR (Navigation Quest Time Routine), which allow you to easily implement a navigation system, time management, and quests. The way NQTR works is based on creating class instances that comply with specific interfaces, which will be registered and provided by the NQTR utilities. In this way, it is possible to create a UI tailored to the game's needs. Alternatively, you can use the base models provided by NQTR and the UI included in the "Point & Click Adventure - React" template to create a complete game in a short time.

Additionally, it is very important to consider that elements can trigger labels (narratives), allowing you to switch to the narrative interface and back.

Crear un nuevo proyecto

El primer paso consiste en crear un nuevo proyecto. Puedes encontrar más información sobre cómo crear un proyecto a partir de una plantilla aquí. We will use the template "Point & Click Adventure - React".

Point & Click Adventure -> React

Una vez completada la creación, es muy importante leer el archivo README.md que se encuentra en la raíz del proyecto. Este archivo contiene información importante sobre el proyecto y sobre cómo utilizarlo.

En nuestro caso, para iniciar el proyecto solo será necesario ejecutar los comandos indicados:

npm install
npm start

Add clickable elements

The main feature of a Point & Click Adventure is the ability to interact with the environment through mouse clicks. To add clickable elements, it is recommended to use UI (in our case React or PixiJS) rather than the Pixi’VN canvas, as it is easier to manage events and interactions.

In the template we are using, when creating the elements that we will see later, there is the possibility to define graphical elements (such as an icon). These graphical elements can be defined using different types; based on this, the system will add the graphical element to the PixiJS UI or to the React UI. In this way, it is possible to create clickable elements in a simple and fast way.

Usually, it is possible to define a graphical element as a string representing the asset alias. The template also provides the possibility to use the TimeSlotsImage class, which allows you to define a different image depending on the time of day.

export const nightcityMap = new Map("nightcity_map", {
    name: "Nightcity",
    background: "map-nightcity", 
    neighboringMaps: {
        south: "main_map",
    },
});

Usually, it is possible to define a graphical element as a React element.

export const bed = new Activity(
    "bed",
    async (_, props) => {
        await props.navigate(NARRATION_ROUTE);
        if (timeTracker.nowIsBetween(5, 22)) {
            await narration.jump(napLabel, props);
        } else {
            await narration.jump(sleepLabel, props);
        }
    },
    {
        name: "bed",
        icon: (activity, props) => { 
            return ( 
                <NqtrRoundIconButton
                    disabled={activity.disabled} 
                    onClick={() => { 
                        activity.run(props).then(() => { 
                            props.invalidateInterfaceData(); 
                        }); 
                    }} 
                    ariaLabel={props.uiTransition(activity.name)} 
                    variant='solid'
                    color='primary'
                    <BedIcon
                        sx={{ 
                            fontSize: { sx: "1.5rem", sm: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" }, 
                        }} 
                </NqtrRoundIconButton> 
            ); 
        }, 
    }
);

Additionally, when the sprite property is available, it is possible to define a graphical element as a PixiJS element.

import { Assets, Sprite, Texture } from "@drincs/pixi-vn/pixi.js";

export const mcHome = new Location("mc_home", mainMap, {
    name: "MC Home",
    sprite: async (location, { navigate }) => { 
        const texture = await Assets.load<Texture>("icon_location_home"); 
        const icon = new Sprite({ 
            texture, 
            x: 300, 
            y: 200, 
            height: 120, 
            width: 120, 
            eventMode: "static", 
            cursor: "pointer", 
        }); 
        icon.on("pointerdown", () => { 
            const entrance = location.entrance; 
            if (entrance) { 
                navigator.currentRoom = entrance; 
                navigate(NAVIGATION_ROUTE); 
            } 
        }); 
        return icon; 
    }, 
});

You can find more detailed documentation about these elements here.

The first step to create a Point & Click Adventure is to create the navigation elements. These elements allow the player to move within the game and interact with the environment.

The navigation system is composed of the following elements:

  • rooms: The core elements of navigation, from which the position of the mc and npc is deduced.
  • locations: A container of rooms.
  • maps: A container of locations.

For example:

values/rooms.ts
import { RegisteredRooms } from "@drincs/nqtr";
import TimeSlotsImage from "../models/TimeSlotsImage";
import Room from "../models/nqtr/Room";
import { bed } from "./activities";
import { mcHome } from "./locations";

export const mcRoom = new Room("mc_room", mcHome, {
    name: "MC room",
    background: new TimeSlotsImage({
        morning: "location_myroom-0",
        afternoon: "location_myroom-1",
        evening: "location_myroom-2",
        night: "location_myroom-3",
    }),
    activities: [bed],
});

RegisteredRooms.add([mcRoom]);
values/locations.ts
import { navigator, RegisteredLocations } from "@drincs/nqtr";
import { Assets, Sprite, Texture } from "@drincs/pixi-vn/pixi.js";
import { NAVIGATION_ROUTE } from "../constans";
import Location from "../models/nqtr/Location";
import { mainMap } from "./maps";

export const mcHome = new Location("mc_home", mainMap, {
    name: "MC Home",
    sprite: async (location, { navigate }) => {
        const texture = await Assets.load<Texture>("icon_location_home");
        const icon = new Sprite({
            texture,
            x: 300,
            y: 200,
            height: 120,
            width: 120,
            eventMode: "static",
            cursor: "pointer",
        });
        icon.on("pointerdown", () => {
            const entrance = location.entrance;
            if (entrance) {
                navigator.currentRoom = entrance;
                navigate(NAVIGATION_ROUTE);
            }
        });
        return icon;
    },
});

RegisteredLocations.add([mcHome]);
values/maps.ts
import { RegisteredMaps } from "@drincs/nqtr";
import TimeSlotsImage from "../models/TimeSlotsImage";
import Map from "../models/nqtr/Map";

export const mainMap = new Map("main_map", {
    name: "Main Map",
    background: new TimeSlotsImage({
        morning: "map-0",
        afternoon: "map-1",
        evening: "map-2",
        night: "map-3",
    }),
    neighboringMaps: {
        north: "nightcity_map",
    },
});

RegisteredMaps.add([mainMap]);

Now, before you can use the navigation utilities, you need to set the current room. After that, it is possible to navigate to the navigation screen at the start of the game or after a short initial narrative.

Additionally, it is important to keep in mind that the current location and map are deduced from the current room, so it is sufficient to set the room to obtain all the necessary information for navigation.

You can find more detailed documentation on how to navigate between UI screens here.

For example:

file_type_ink
ink/start.ink
=== start ===
    # navigate /narration
    Hello, welcome to the my first game!

    # enter room mc_room
    # navigate /navigation
    -> DONE

Time system

You can find more detailed documentation about these elements here.

Now let's move on to another fundamental element: the time management system. The time management system is a key component in a Point & Click Adventure, as it allows you to create a dynamic and realistic world where the player's actions can have different consequences depending on the time of day.

Before you can use the time management features, it is necessary to initialize the timeTracker with the information required for the system to function. As shown in the following example, it is possible to define every aspect of the time management system, such as time slots, days of the week, the start and end time of the day, and so on.

import { timeTracker } from "@drincs/nqtr";
import { timeSlots } from "../constans";

const weekDaysNames = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"];
export function initializeNQTR() {
    timeTracker.initialize({
        defaultTimeSpent: 1,
        dayStartTime: 0,
        dayEndTime: 24,
        timeSlots: [
            { name: timeSlots.morning.description, startTime: timeSlots.morning.value },
            { name: timeSlots.afternoon.description, startTime: timeSlots.afternoon.value },
            { name: timeSlots.evening.description, startTime: timeSlots.evening.value },
            { name: timeSlots.night.description, startTime: timeSlots.night.value },
        ],
        getDayName: (weekDayNumber: number) => {
            return weekDaysNames[weekDayNumber];
        },
        weekendStartDay: 6,
        weekLength: 7,
    });
}

The main functionality of the time management system is the ability to advance time based on the player's actions.

file_type_ink
index.ink
// {timeValue} is a number representing the amount of time to increment
// {dateValue} is a number representing the amount of days to increment
# wait {timeValue} // Increment time by {value}
# wait // Increment time by defaultTimeSpent
# wait days {dateValue} // Increment date by {dateValue} and set time to dayStartTime
# wait days {dateValue} hours {timeValue} // Increment date by {dateValue} and time by {timeValue}

Activity

You can find more detailed documentation about these elements here.

A fundamental element in Point & Click Adventures is interactive elements, which allow the player to interact with the environment and progress the story. In NQTR, these elements are represented by activity. activity are elements that can be executed by the player and can have different consequences depending on the context in which they are performed.

For example:

import { RegisteredActivities, timeTracker } from "@drincs/nqtr";
import { narration } from "@drincs/pixi-vn";
import BedIcon from "@mui/icons-material/Bed";
import NqtrRoundIconButton from "../components/NqtrRoundIconButton";
import { NARRATION_ROUTE } from "../constans";
import { napLabel, sleepLabel } from "../labels/sleepNapLabels";
import Activity from "../models/nqtr/Activity";

export const bed = new Activity(
    "bed",
    async (_, props) => {
        await props.navigate(NARRATION_ROUTE);
        if (timeTracker.nowIsBetween(5, 22)) {
            await narration.jump(napLabel, props);
        } else {
            await narration.jump(sleepLabel, props);
        }
    },
    {
        name: "bed",
        icon: (activity, props) => {
            return (
                <NqtrRoundIconButton
                    disabled={activity.disabled}
                    onClick={() => {
                        activity.run(props).then(() => {
                            props.invalidateInterfaceData();
                        });
                    }}
                    ariaLabel={props.uiTransition(activity.name)}
                    variant='solid'
                    color='primary'
                >
                    <BedIcon
                        sx={{
                            fontSize: { sx: "1.5rem", sm: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" },
                        }}
                    />
                </NqtrRoundIconButton>
            );
        },
    },
);

RegisteredActivities.add([bed]);

Activities are connected to rooms in a many-to-many relationship, meaning that an activity can be present in multiple rooms and a room can contain multiple activities.

To use them, you must link them to a room through the activities property. This way, when the player is in that room, they will be able to see the activity and interact with it.

Alternatively, during the narrative session, it is possible to add an activity to a room using the addActivity function.

For example:

== start ==
# add activity bed in mc_room

Routine

You can find more detailed documentation about these elements here.

Routine refers to the set of elements that make up the daily routine of non-player characters (NPC).

The routine is composed of commitment, which represent a character’s obligation to perform a specific activity at a certain moment of the day. These commitments are very similar to activity, as they represent an activity that the character performs.

For example:

import { RegisteredCommitments } from "@drincs/nqtr";
import { narration } from "@drincs/pixi-vn";
import QuestionAnswerIcon from "@mui/icons-material/QuestionAnswer";
import NqtrRoundIconButton from "../components/NqtrRoundIconButton";
import { NARRATION_ROUTE } from "../constans";
import { TALK_SLEEP_LABEL_KEY } from "../labels/variousActionsLabelKeys";
import Commitment from "../models/nqtr/Commitment";
import { alice } from "./characters";

export const aliceSleep = new Commitment("alice_sleep", alice, {
    priority: 1,
    timeSlot: {
        from: 20,
        to: 10,
    },
    background: "alice_roomsleep0A",
    icon: (commitment, props) => {
        return (
            <NqtrRoundIconButton
                disabled={commitment.disabled}
                onClick={() => {
                    if (commitment.run) {
                        commitment.run(props);
                    }
                }}
                ariaLabel={commitment.name}
                variant='solid'
                color='primary'
            >
                <QuestionAnswerIcon
                    sx={{
                        fontSize: { sx: "1.5rem", sm: "2rem", md: "2.5rem", lg: "3rem", xl: "3.5rem" },
                    }}
                />
            </NqtrRoundIconButton>
        );
    },
    onRun: async (_, event) => {
        await event.navigate(NARRATION_ROUTE);
        await narration.jump(TALK_SLEEP_LABEL_KEY, event);
    },
});

RegisteredCommitments.add([aliceSleep]);

Commitments are linked to rooms in a one-to-many relationship, meaning that a room can have multiple commitments, but a commitment is present in only one room (a character cannot be in multiple rooms at the same time).

To use them, you need to link them to a room through the commitments property. This will also set the NPC positions, as the position of a commitment is deduced from the room it is linked to. When the player is in that room, they will see the active commitments and be able to interact with them.

Alternatively, during the narrative session, it is possible to add a commitment to a room using the addCommitment function.

For example:

== start ==
# add routine alice_sleep in alice_room

Quests

You can find more detailed documentation about these elements here.

Quest represent an objective or a series of objectives that the player can complete during the game. Quests are composed of stage, which represent the different phases of a quest. Each stage has a specific objective and can be completed in different ways depending on the player's choices.

For example:

import { RegisteredCommitments, RegisteredQuests, routine } from "@drincs/nqtr";
import { narration } from "@drincs/pixi-vn";
import { NARRATION_ROUTE } from "../../constans";
import { TALK_ALICE_QUEST_KEY } from "../../labels/variousActionsLabelKeys";
import TimeSlotsImage from "../../models/TimeSlotsImage";
import Commitment from "../../models/nqtr/Commitment";
import Quest from "../../models/nqtr/Quest";
import Stage from "../../models/nqtr/Stage";
import { orderProduct, takeProduct } from "../activities";
import { alice } from "../characters";
import { mcRoom, terrace } from "../rooms";

export const aliceQuest = new Quest(
    "aliceQuest",
    [
        // stages
        new Stage("talk_alice1", {
            onStart: () => {
                terrace.addCommitment(aliceQuest_talk);
            },
            name: "Talk to Alice",
            description: "Talk to Alice on the terrace",
        }),
        new Stage("order_products", {
            onStart: () => {
                mcRoom.addActivity(orderProduct);
            },
            name: "Order products",
            description: "Order the products with your PC",
        }),
        new Stage("take_products", {
            onStart: (_, { notify }) => {
                terrace.addActivity(takeProduct);
                notify("You can take the products on the Terrace");
            },
            name: "Take products",
            description: "Take products on the Terrace",
            requestDescriptionToStart: "Wait for the products you ordered to arrive (2 day)",
            deltaDateRequired: 2,
        }),
        new Stage("talk_alice2", {
            name: "Talk to Alice",
            description: "Talk to Alice on the terrace",
        }),
    ],
    {
        // props
        name: "Help Alice",
        description:
            'To learn more about how the repo works, Talk to Alice. \nGoing when she is there will automatically start an "Event" (see aliceQuest.tsx to learn more). \nAfter that an action will be added to open the pc, in MC room. \n\n(during the quest you can talk to Alice and you will see her talking during the quests of the same Quest)',
        image: "alice_terrace0A",
        onStart: (quest, { notify, uiTransition }) => {
            notify(uiTransition("notify_quest_is_started", { quest: quest.name }));
        },
        onContinue: (stage, { notify, uiTransition }) => {
            notify(uiTransition("notify_quest_is_updated", { quest: stage.name }));
        },
    },
);

RegisteredQuests.add(aliceQuest);

const aliceQuest_talk = new Commitment("alice_quest_talk", alice, {
    timeSlot: {
        from: 10,
        to: 20,
    },
    image: new TimeSlotsImage("alice_terrace0A"),
    executionType: "automatic",
    priority: 1,
    onRun: async (_, props) => {
        await props.navigate(NARRATION_ROUTE);
        await narration.jump(TALK_ALICE_QUEST_KEY, props);
        routine.remove(aliceQuest_talk);
    },
});

RegisteredCommitments.add(aliceQuest_talk);

To start a quest, you need to call the start function on the quest itself. This will begin the first stage of the quest, allowing the player to start completing its objectives.

For example:

file_type_ink
ink/start.ink
== start ==
# start quest aliceQuest

To continue a quest that has already been started, you need to call the continue function on the quest itself. This will complete the current stage of the quest and move on to the next one.

For example:

file_type_ink
ink/start.ink
== start ==
# continue quest aliceQuest

During gameplay or narrative sequences, it is very useful to perform checks on the state of a quest, such as verifying whether a quest has been completed or if a specific stage has been reached. To do this, you can use various query functions available in the Quest class, such as completed or failed.

You can find a complete list of query functions in the Quest class documentation here.

For example:

file_type_ink
ink/variousActionsLabels.ink
=== talkAliceQuest ===
# show image background alice_terrace0At
{ aliceQuest_currentStageIndex:
- 0:  -> talkAliceQuest0
- 1:  -> talkAliceQuest1
- 2:  -> talkAliceQuest2
- else: alice: Thanks for the book.
}
-> DONE

= talkAliceQuest0
alice: Hi, can you order me a new book from pc?
mc: Ok
alice: Thanks
# continue quest aliceQuest
-> DONE

= talkAliceQuest1
mc: What book do you want me to order?
alice: For me it is the same.
-> DONE

= talkAliceQuest2
mc: I ordered the Book, hope you enjoy it.
alice: Great, when it arrives remember to bring it to me.
-> DONE

= talkAliceQuest3
mc: Here's your book.
alice: Thank you, I can finally read something new.
# continue quest aliceQuest
-> DONE

Conclusión

Now you know the basics for creating a Point & Click Adventure with Pixi’VN and NQTR. So far, we have only looked at the components provided by the template, but it is possible to create custom components to adapt them to the needs of your game. If you have some experience with JS/TS, I’m confident that by modifying the .d.ts files and creating classes that implement NQTR interfaces, you will be able to create custom components quickly and easily.

Make me proud by creating an amazing Point & Click Adventure with Pixi’VN and NQTR! 🚀

Now, you can try a small Point & Click Adventure created with the template to see in practice how the components work and interact with each other.

On this page