Skip to main content

KorGE Tutorial - Writing 2048 game. Step 1 - Views

In the previous step we have created and configured a KorGE project for our game. In this step, we'll discuss KorGE's views and their positioning. We will also fill our stage with static elements using views, such as Graphics, Container, RoundRect, Text and Image. At the end of this step we will have the following result:


So let's start!

Views and containers

View structure in KorGE is based on two types of elements: views and containers. A container is a view that can have other views as children. During rendering, KorGE goes through the elements of the root container (the game scene) and draws them in the order they're added to the view structure. The element added first will be covered by the other element added after it and overlapping it:


Each view has a size (width and height) and a position (x and y). Width and height determine the size of the rectangle bounding this view, x and y determine its position inside the parent container.

Main sizes

Before discussing various views, we should define a few main sizes. In the game, we'll have many square blocks (cells) with the same width/height. I suggest you use the cell size as the base size, it will simplify the determining other view sizes. The simplest way of calculating the most suitable size for a cell is by reducing the size of the scene by an integer number of times. Since we need 4 cells and small indents between them to fit on the screen, we can divide the size of the scene by 5 (we get the scene width via views.virtualWidth, read more here):
val cellSize = views.virtualWidth / 5.0
Let's define cell indents equal to 10 px. Now we can calculate the size of the whole field and the indent from the left edge of the screen:
val fieldSize = 50 + 4 * cellSize
val leftIndent = (views.virtualWidth - fieldSize) / 2
The last size value we need to specify is the indent from the top edge of the screen. Just set it to 150.0 (the value was determined experimentally):
val topIndent = 150.0
Now we can take a look at some KorGE views and use them.

RoundRect

Some of the most common views in KorGE are Graphics and its derived elements (like RoundRect and Circle). We'll take a look at Graphics later. Now, to understand the concept of creating korge views, we start with a simpler view - RoundRect.

Let's try to draw our first dark rectangle that is the background of the game field. We create a new RoundRect object and specify its width and height, rounding radius and color. Then we specify its position via x and y:
val bgField = RoundRect(fieldSize, fieldSize, 5.0, color = Colors["#b9aea0"])
bgField.x = leftIndent
bgField.y = topIndent

Let's run the runJvm task in terminal or via green play button (as I wrote in the previous step). And what do we see?


Nothing! There's no rectangle on the screen. But why?

We forgot to add bgField to the root container! Let's fix it and restart the game.
val bgField = RoundRect(fieldSize, fieldSize, 5.0, color = Colors["#b9aea0"])
bgField.x = leftIndent
bgField.y = topIndent
addChild(bgField)
//or alternatively
//bgField.addTo(this)


Yeah! It works! But what if we use tens or hundreds of view in our game? Do we need to add all of them to the root container manually and maybe repeat this mistake many times? No!

View DSL

KorGE provides a convenient way of working with a structured view tree. It uses inline and extension functions of Kotlin to create a special View DSL. Let's rewrite the creation of bgField using it:
val bgField = roundRect(fieldSize, fieldSize, 5.0, color = Colors["#b9aea0"]) {
    x = leftIndent
    y = topIndent }
Do you see the difference? Instead of constructor RoundRect(...) we use a special function roundRect(...) {...} that has the same signature, but it adds the view to the container by itself. We also provide a code block (in the curly braces) as the last parameter and specify the coordinates in it (using this.x instead of bgField.x). We still assign the roundRect object to the variable bgField because we'll need this object later.

We can also simplify this code a bit using a special function - position(x, y):
val bgField = roundRect(fieldSize, fieldSize, 5.0, color = Colors["#b9aea0"]) {
    position(leftIndent, topIndent) }

So we have a simpler and easier-to-read code that does the same (you can rerun the game and check). From now on we will use only View DSL to build the view tree because it's the recommended approach.

Graphics

Graphics lets you draw lines, shapes and complex contours. It has several methods for that, but we'll take a look only at a few of them.

The Graphics object has its own DSL. The main functions are fill, stroke and fillStroke. There are also extension functions like rect, rectHole, roundRect, arc, circle and ellipse.

Here we create the first cell background using some of them:
graphics {
position(leftIndent, topIndent)
fill(Colors["#cec0b2"]) {
roundRect(10, 10, cellSize, cellSize, 5.0)
}
}
Here we position our Graphics object, specify the color via fill function and draw a rounded rectangle with it. We specify the rectangle position relative to its Graphics container, not the root container! We also specify its size and rounding radius.

Let's look at the result:


It worked!

Now we need to draw other 15 cells. Do you want to draw them in sequence? Let's do it easier and take advantage of two for cycles:
graphics {
position(leftIndent, topIndent)
fill(Colors["#cec0b2"]) {
for (i in 0..3) {
for (j in 0..3) {
roundRect(10 + (10 + cellSize) * i, 10 + (10 + cellSize) * j, cellSize, cellSize, 5.0)
}
}
}
}

So let's look at the result:


Well done!

Next we'll add elements at the top: the game logo and two blocks for the current score and the best score (we will change their values later).

First, let's define the logo background. You should already understand this code:
val bgLogo = roundRect(cellSize, cellSize, 5.0, color = Colors["#edc403"]) {
position(leftIndent, 30.0)
}


We can add score blocks the same way. We'll make them a little wider and a little shorter than the cells. But then we'll need to calculate their coordinates manually and that's a lot of unnecessary code... We can do that easier!

Relative positioning

KorGE provides a convenient way of specifying a position of a view relative to other views. There are extension functions like alignRightToLeftOf, alignTopToBottomOf, centerBetween and centerOn. Let's use some of them to create and position score blocks:
val bgBest = roundRect(cellSize * 1.5, cellSize * 0.8, 5.0, color = Colors["#bbae9e"]) {
alignRightToRightOf(bgField)
alignTopToTopOf(bgLogo)
}
val bgScore = roundRect(cellSize * 1.5, cellSize * 0.8, 5, color = Colors["#bbae9e"]) {
alignRightToLeftOf(bgBest, 24)
alignTopToTopOf(bgBest)
}


The next part of this step – text elements.

Text

KorGE has a simple view for displaying text - text. But we also need a bitmap font for it. I've decided to use Clear Sans font. You can find the needed files (clear_sans.fnt and clear_sans.png) in my repository on GitHub. You should add these files to your resources folder in commonMain. Then we need to import the font before using it (I prefer to do it at the beginning of the main function):
val font = resourcesVfs["clear_sans.fnt"].readBitmapFont()

Now we can add a text view for our logo. We specify a text string, a text size, a text color and a font. We use centerOn function to center it on our logo background:
text("2048", cellSize * 0.5, Colors.WHITE, font).centerOn(bgLogo)


Now we can add text views for the current score and the best score the same way. We'll take advantage of alignTopToTopOf function and specify indent between two views as the second argument. We'll also specify text bounds for score number views and align them, so they will be shown correctly with different numbers.
text("BEST", cellSize * 0.25, RGBA(239, 226, 210), font) {
centerXOn(bgBest)
alignTopToTopOf(bgBest, 5.0)
}
text("0", cellSize * 0.5, Colors.WHITE, font) {
setTextBounds(Rectangle(0.0, 0.0, bgBest.width, cellSize - 24.0))
format = format.copy(align = Html.Alignment.MIDDLE_CENTER)
alignTopToTopOf(bgBest, 12.0)
centerXOn(bgBest)
}
text("SCORE", cellSize * 0.25, RGBA(239, 226, 210), font) {
centerXOn(bgScore)
alignTopToTopOf(bgScore, 5.0)
}
text("0", cellSize * 0.5, Colors.WHITE, font) {
setTextBounds(Rectangle(0.0, 0.0, bgScore.width, cellSize - 24.0))
format = format.copy(align = Html.Alignment.MIDDLE_CENTER)
centerXOn(bgScore)
alignTopToTopOf(bgScore, 12.0)
}


So, there remains the last part of UI that we must implement in this step – buttons undo and restart. We'll implement them as simple rectangles with images over them.

Images

KorGE has a great support of images. But it's a topic for a separate article. In this tutorial we'll consider images only like simple views. First, you should prepare image files in your resources folder (you can find my files on GitHub). Then we need to import these images in the game:
val restartImg = resourcesVfs["restart.png"].readBitmap()
val undoImg = resourcesVfs["undo.png"].readBitmap()

Now let's create our buttons. Here we'll use container {...} block for each button and add a background and an image to it. It will help us specify onClick events in the next step. This way a user will be able to click on a whole container as a button.
val btnSize = cellSize * 0.3
val restartBlock = container {
val background = roundRect(btnSize, btnSize, 5.0, color = RGBA(185, 174, 160))
image(restartImg) {
size(btnSize * 0.8, btnSize * 0.8)
centerOn(background)
}
alignTopToBottomOf(bgBest, 5)
alignRightToRightOf(bgField)
}
val undoBlock = container {
val background = roundRect(btnSize, btnSize, 5.0, color = RGBA(185, 174, 160))
image(undoImg) {
size(btnSize * 0.6, btnSize * 0.6)
centerOn(background)
}
alignTopToTopOf(restartBlock)
alignRightToLeftOf(restartBlock, 5.0)
}

So we have the final result for this step:


The whole code written in this step is shown here.
In the next step we'll add dynamic blocks and implement interaction with them. Stay tuned!

Comments

  1. This comment has been removed by the author.

    ReplyDelete
  2. Wow, what a superb content! Thanks for sharing this really important KorGE tutorial with us. Keep it up!
    Games I Can Use To Start A Game Portal

    ReplyDelete

Post a Comment

Popular posts from this blog

KorGE Tutorial - Writing 2048 game. Step 0 - Introduction

KorGE  is an  open source  multiplatform game engine written in Kotlin . You can use it to develop your own games of various genres targeting JVM, Web, Android and iOS. In this tutorial I will guide you through the steps of writing a 2048 game using KorGE and Kotlin. You can check the resulting game  here . The source code is  on GitHub . This tutorial will have several parts - steps. In them, we will discuss the following features of KorGE: Views and graphics Text and images Positioning Mouse and keyboard events Animation Native Storage and more! Well, let's get started! Note: you need to install Java JDK and Intellij IDEA before start. Creating new project First, we need to create a new project for our game. There are two ways to do it: 1. Using KorGE Intellij plugin KorGE provides an IntelliJ plugin, that allows you to create KorGE projects. You can read this guide  or watch this video to know how to install the plugin and create a new

KorGE 1st Game Jam - 12-June - 14-June

Some updates about the KorGE organization: After a few years in development and lots of fixes and improvements, KorGE is getting mature. In April, Mikhail Reznichenko  and Nicolai Emig joined the KorGE organization. Mikhail Reznichenko, our member from Russia, has mostly worked on the technical part of KorGE and other korlibs and has given support at slack, he has done a remarkable work on the UI and other parts of the engine, helped with the intelliJ plugin and also created a 2048 clone as a sample of using KorGE. Nicolai Emig, our member from Germany, created an educational wrapper around KorGE and he is teaching it to his students, he has helped at slack, promoting the engine, and additionally he has contributed to the core with ideas and code including Sprite Animations and some samples to the core, he has also helped with organization of the Game JAM. In addition,  Tamara Gadea , joined as a sporadic contributor helping us some graphical assets, including the on