Skip to content

Learning the Basics

eboatwright edited this page Mar 5, 2022 · 14 revisions

Learning the Basics

I know this looks really long, but don't be overwhelmed! I've tried to split all the different topics into bite-sized sections.
You've got this! 😄

Installation

If you haven't installed JSCII already, follow the instructions here.

Basic Game Loop

In your game's JavaScript file, override the init(), update(), and render() functions.
Note that init() has to return true at the end.

init = function() {
    return true;
}

update = function() {
}

render = function() {
}

These functions will automatically be called in the right order.

Basic Rendering

In your render() function put this code for rendering a char to the screen.

render = function() {
    FONT.renderChar(AT, 5, 5, WHITE, BLACK);
}

This will render an @ at the tile coordinates 5, 5 with a white color, on a black background.
(Check the constants.js file / section for all the characters)

Player Movement and Input

For player movement, we'll make a variable called playerPosition at the top of our script.

var playerPosition = vector2(5, 5);

We'll then, check for keyboard input in update()

update = function() {
    if(keyDown("ArrowUp"))
        playerPosition.y -= 1;
    if(keyDown("ArrowDown"))
        playerPosition.y += 1;
    if(keyDown("ArrowLeft"))
        playerPosition.x -= 1;
    if(keyDown("ArrowRight"))
        playerPosition.x += 1;
}

Then, in render() we'll change the render position to playerPosition.

render = function() {
    FONT.renderChar(AT, playerPosition.x, playerPosition.y, WHITE, BLACK);
}

Entities

So. Let's refactor our script to use JSCII's Entities and Components.

First, let's make an Entity for the player.

class Player extends Entity {
    constructor(position = vZero()) {
        super("player", position, ["player"]);
    }

    init() {
    }

    update() {
    }

    render() {
    }
}

JSCII's Entity and Component system, is pretty clean I think :) (Let me know what you think)
You'll also notice, the little vZero() That's shorthand for vector2(0, 0). The super() call, has 3 parameters. ID, Position, and Tags. It's always a good idea, (and required with some "engine" features of JSCII) that the player has a "player" tag. Let's continue with the update() and render() functions.

update() {
    if(keyDown("ArrowUp"))
        this.position.y -= 1;
    if(keyDown("ArrowDown"))
	this.position.y += 1;
    if(keyDown("ArrowLeft"))
	this.position.x -= 1;
    if(keyDown("ArrowRight"))
	this.position.x += 1;
}

This checks for the up, down, left and right cursor / arrow keys, and move's the entity accordingly.
Now for rendering.

render(level) {
    FONT.renderChar(AT, this.position.x, this.position.y, WHITE, BLACK);
}

Now let's refactor the rest of the code! Here's the finished code so far.

class Player extends Entity {
    constructor(position = vZero()) {
        super("player", position, ["player"]);
    }

    init() {
    }

    update() {
        if(keyDown("ArrowUp"))
            this.position.y -= 1;
        if(keyDown("ArrowDown"))
    	    this.position.y += 1;
        if(keyDown("ArrowLeft"))
	    this.position.x -= 1;
        if(keyDown("ArrowRight"))
	    this.position.x += 1;
    }

    render() {
        FONT.renderChar(AT, this.position.x, this.position.y, WHITE, BLACK);
    }
}

var player;

init = function() {
    player = new Player(vector2(5, 5));
    player.init();
    return true;
}

update = function() {
    player.update();
}

render = function() {
    player.render();
}

Level

Now, if you made an entire game doing everything like this, it would get cluttered very fast.

So, how do we fix it? In JSCII, we use a Level.
It's like a Scene in Unity or Godot, but it's called a Level because it handles other stuff too.

Let's incorporate this into our little game.

var level;

...

init = function() {
    level = new Level();
    level.addEntity(new Player(vector2(5, 5)));
    level.init();
    return true;
}

update = function() {
    level.update();
}

render = function() {
    level.render();
}

We start out, by removing the player variable, and making a variable named level. Then, we create it in the init function, (I do this so that if you need to restart the game, you can just call init()) then we call level.init(), level.update() and level.render() in init(), update() and render().

Next, we need to do something in the Player Entity. The init(), update() and render() functions of an Entity, actually get level as a parameter! Let's add that.

class Player extends Entity {
    ...
    init(level) {
    }

    update(level) {
        ...

    render(level) {
        ...

Components

So now if we tried to make a game with this... it would still be pretty cluttered.

So what else can we do? Components. Components are things we can add to Entities, to add more functionality. So, what functionality do we have? Player movement. So, let's make a PlayerController component!

class PlayerController extends Component {
    constructor(entity) {
        super(entity);
    }

    update(level) {
	if(keyDown("ArrowUp"))
	    this.entity.position.y -= 1;
	if(keyDown("ArrowDown"))
	    this.entity.position.y += 1;
	if(keyDown("ArrowLeft"))
            this.entity.position.x -= 1;
	if(keyDown("ArrowRight"))
	    this.entity.position.x += 1;
    }
}

Each component takes in what entity it is attached to. Then, it passes it to the super() and the Component class will handle it.
Components also have an init(), update() and render(), though just like Entity, you don't have to use them all.

So how do we add Components to Entities? You might be thinking. Well, this is how!

class Player extends Entity {
    constructor(position = vZero()) {
        super("player", position, ["player"]);
        this.controller = new PlayerController(this);
        this.renderer = new CharRenderer(this, "default", AT, WHITE, BLACK);
    }
    ...
    update(level) {
        this.controller.update(level);
    }
    
    render(level) {
        this.renderer.render(level);
    }
}

We just add it as a property of the Entity! JSCII also comes with a CharRenderer Component, so we add that to the Player too.
The CharRenderer takes 5 parameters. Entity, RenderLayer, Char, Foreground Color, and Background Color. The render layer is initialized in the Level when you create a new one. Although, you'll notice, that we didn't pass anything through. That's because, Level has 3 default render layers. Default, lighting, and ui. (It renders in the order: default first, then lighting, then ui. So UI is on top, and default is on bottom)

Here's all the code so far!

class PlayerController extends Component {
    constructor(entity) {
        super(entity);
    }

    update(level) {
        if(keyDown("ArrowUp"))
            this.entity.position.y -= 1;
        if(keyDown("ArrowDown"))
            this.entity.position.y += 1;
        if(keyDown("ArrowLeft"))
            this.entity.position.x -= 1;
        if(keyDown("ArrowRight"))
            this.entity.position.x += 1;
    }
}

class Player extends Entity {
    constructor(position = vZero()) {
        super("player", position, ["player"]);
        this.controller = new PlayerController(this);
        this.renderer = new CharRenderer(this, "default", AT, WHITE, BLACK);
    }

    update(level) {
        this.controller.update(level);
    }

    render(level) {
        this.renderer.render(level);
    }
}

var level;

init = function() {
    level = new Level();
    level.addEntity(new Player(vector2(5, 5)));
    level.init();
    return true;
}

update = function() {
    level.update();
}

render = function() {
    level.render();
}

Tilemap

Our game is looking a little empty. So, let's add a tilemap!
Adding a Tilemap, is very easy in JSCII, because the Level class handles it all for you!

// If you haven't read the past sections, look at those to get some context :)

const TILESET = [
    new Tile(SPACE, BLACK, BLACK, []),
    new Tile(PERIOD, MID_DARK_GREEN, BLACK, []),
    new Tile(HASH, MID_GRAY, DARK_GRAY, ["solid"]),
];

...
    var tiles = [
        [0, 1, 0, 1, 0],
        [0, 2, 0, 0, 0],
        [1, 2, 2, 1, 1],
        [1, 0, 1, 1, 1],
        [0, 0, 1, 0, 0],
    ];
    level = new Level(["default"], new Tilemap("tilemap", TILESET, tiles));
...

So, we start off by making a constant, TILESET. This keeps track of all the different tiles in our game.
Tile takes in 4 parameters. Char, Foreground Color, Background Color, and Tags.
(Notice, that the last one has a "solid" tag)
So, these tiles represent: empty space, grass, and a wall.

Next, is the tiles. (I'm only using a 5x5 tilemap for simplicity's sake, but you can make yours as big as you want)
You're probably wondering what the numbers mean. Well, each number is the index of the Tile it represents. So, the 0 is the first one, 1 is the second, and 2 is the third!

Then you'll notice that the level is different now. I'm passing in only 1 render layer, (default) for simplicity, because we don't have any UI. (Maybe in a future tutorial ;))
Then, we pass a tilemap straight through! We're inputing 3 parameters into Tilemap: ID, tileset, and tiles. (Tilemap also has other parameters, but we're only using these 3).

And just like that, Level handles everything for us! (How handy! ;))

Collisions and Actions

You might try it out, and notice that the wall doesn't stop the player! So let's implement collisions with JSCII's built in Actions!

An Action, (Just like Entity and Component) is a class that you extend to make your own. JSCII comes with a basic MoveAction that you can use for simple prototypes, that handles tilemap collisions!
Let's implement that.

class PlayerController extends Component {
    ...
    update(level) {
        var direction = vector2(0, 0);
        if(keyDown("ArrowUp"))
            direction.y -= 1;
        if(keyDown("ArrowDown"))
            direction.y += 1;
        if(keyDown("ArrowLeft"))
            direction.x -= 1;
        if(keyDown("ArrowRight"))
            direction.x += 1;

        new MoveAction(this.entity, level, direction).perform();
    }
}

So this is basically an overhaul of the PlayerController, so let's cover it again.
First, we create a direction vector2, and modify that with the keyboard input instead of directly controlling the Entity's position.
Next, we create a MoveAction() with the Entity that this component belongs to, the level, and the direction the player wants to move.

Then, we immediately perform that action. An Action can be performed as many times as you want, though it will use the same parameters.

Also, something that you should know about the MoveAction, is that it will only recognize something as a wall, if the tile has the tag "solid". So, double-check that and make sure you have that. :)

End

And that's the end of this tutorial! Props to you if you made it all the way through, and I hope you like JSCII! More tutorials, and documentation are coming soon, so stay tuned!

Next: Documentation

Clone this wiki locally