Learning Basic For Total Beginners (BlitzMax NG)

Started by Midimaster, December 22, 2021, 18:29:29

Previous topic - Next topic

Midimaster

#15
Lesson XV: Some Mazes need Bits

When you compare picture of maze boards with our maze from the last lesson, you can find those, where the walls are very thin.



Here the walls are no standalone elements in the array: they now share a field with other game elements like player or gold or hidden keys at the same time.

Here two walls and a object sharing the same field:





but how can it be that a single variable stores two or three values?

Variables are organized in BITs

A INTEGER variables consists of 32bits. We can use them indiviually to store 32 different game elements.

test this:
Code (BlitzMax) Select
Global a:Int = 4+1
Print Bin(a)

output:
00000000000000000000000000000101



Bin(n:Int) shows the content of a variable as BITs


You can see that the number "1" got the very right BIT and the number "4" got the third bit.
As long as you use only this numbers: 1 , 2 , 4 , 8 , 16 , 32 , 64 ,... the system works without affecting the other bits.

In our new system we would not longer define the elements like here:
1 = Wall
2 = Player
3 = Gold
4 = Exit

but now this way:

1 = Top Wal
2 = Left Wall
4 = Player
8 = Gold
16 = Exit
...


So we can store a wall and a gold (1+4) together into the same field. But we are not allowed to use mathematic operations like "+" or "-" to manipulate such bits!!!

Therefore we use...

BIT-Operations

BIT-wise OR adds this element to the variable
Code (BlitzMax) Select
Variable = Variable | element


BIT-wise AND checks whether this element is in the variable
Code (BlitzMax) Select
If  (Variable & element)=element

BIT-wise AND XOR removes this element from the the variable
Code (BlitzMax) Select
variable = Variable & ~ element

for Bitwise operations we need this three letters:
& = BIT AND
| = BIT OR
~ = BIT XOR
&~ = (BIT REMOVE)

We can simplify this for us by defining three functions:
Code (BlitzMax) Select
Function Add(X:Int, Y:Int, element:Int)
Board[x,y] = Board[x,y] | element
End Function


Function Remove(X:Int, Y:Int, element:Int)
Board[x,y]=Board[x,y] &~ element
End Function


Function Check:Int(X:Int, Y:Int, element:Int)
Return (Board[x,y] & element) = element
End Function



New Version of Our Maze

Now we have to re-write the maze from last lesson.

Walls are now made this way:
Code (BlitzMax) Select
' elements:
' 1 = top wall
' 2 = left wall
' 4 = player
' 8 = gold

For Local I:Int=1 To 10
Add i, 1 , 1
add i,11 , 1
Next
For Local I:Int=1 To 10
Add 1, i , 2
add 11,i , 2
Next

For Local i:Int=0 To 40
Add  Rand(1,10) , Rand(1,10)  , 1
Add  Rand(1,10) , Rand(1,10)  , 2
Next



The Drawing look like this:

Code (BlitzMax) Select
If Check(x,y , 1)
SetColor 105,80,65
DrawRect x*size , y*size-3, size, 6
EndIf

If Check(x,y , 2)
SetColor 105,80,65
DrawRect x*size-3 , y*size, 6, size
EndIf

If Check(x,y , 4)
SetColor 255,255,255
DrawImage Player, x*size , y*size
EndIf

If Check(x,y , 8)
SetColor 200,150,0
DrawOval x*size+10 , y*size+10,20,20
EndIf



And the moving of the player has changed:

Code (BlitzMax) Select
Function FindAndMovePlayer(moveX:Int, moveY:Int)
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
If Check (x,y,4)

If     moveX=-1
If Check(   x,y   ,2 ) Return
ElseIf moveX=1
If Check( 1+x,y   ,2 ) Return
ElseIf moveY=-1
If Check(   x,y   ,1 ) Return
ElseIf MoveY=1
If Check(   x,y+1 ,1 ) Return
EndIf

Add x+moveX ,y+moveY,4
Remove x,y, 4
Return
EndIf
Next
Next
End Function



The whole code:

Code (BlitzMax) Select
SuperStrict
Global Board:Int[12,12]
Graphics 800,650
SetBlend alphablend
Global Player:TImage=LoadImage("player.png")   

' elements:
' 1 = top wall
' 2 = left wall
' 4 = player
' 8 = gold

For Local I:Int=1 To 10
Add i, 1 , 1
add i,11 , 1
Next
For Local I:Int=1 To 10
Add 1, i , 2
add 11,i , 2
Next

For Local i:Int=1 To 40
Add  Rand(1,10) , Rand(1,10)  , 1
Add  Rand(1,10) , Rand(1,10)  , 2
Next


Add 5,5 , 4
Add 6,8 , 8

Global Size:Int=50
Repeat
Cls
For Local y:Int= 1 To 11
For Local x:Int= 1 To 11

If Check(x,y , 1)
SetColor 105,80,65
DrawRect x*size , y*size-3, size, 6
EndIf

If Check(x,y , 2)
SetColor 105,80,65
DrawRect x*size-3 , y*size, 6, size
EndIf

If Check(x,y , 4)
SetColor 255,255,255
DrawImage Player, x*size , y*size
EndIf

If Check(x,y , 8)
SetColor 200,150,0
DrawOval x*size+10 , y*size+10,20,20
EndIf

Next
Next
CheckPlayer
Flip
Until AppTerminate()


Function CheckPlayer()
If KeyHit(KEY_LEFT)
FindAndMovePlayer(-1,0)
ElseIf KeyHit(KEY_RIGHT)
FindAndMovePlayer(+1,0)
ElseIf KeyHit(KEY_UP)
FindAndMovePlayer(0,-1)
ElseIf KeyHit(KEY_DOWN)
FindAndMovePlayer(0,+1)
EndIf
End Function


Function FindAndMovePlayer(moveX:Int, moveY:Int)
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
If Check (x,y,4)

If     moveX=-1
If Check(   x,y   ,2 ) Return
ElseIf moveX=1
If Check( 1+x,y   ,2 ) Return
ElseIf moveY=-1
If Check(   x,y   ,1 ) Return
ElseIf MoveY=1
If Check(   x,y+1 ,1 ) Return
EndIf

Add x+moveX ,y+moveY,4
Remove x,y, 4
Return
EndIf
Next
Next
End Function


Function Add(X:Int, Y:Int, element:Int)
Board[x,y] = Board[x,y] | element
End Function


Function Remove(X:Int, Y:Int, element:Int)
Board[x,y]=Board[x,y] &~ element
End Function


Function Check:Int(X:Int, Y:Int, element:Int)
Return (Board[x,y] & element) = element
End Function



Challenge I: Snake-Game

Code a snake game. In an empty maze a short worm becomes longer and longer like a snake, while it is moving. The user trys to steer the snake, that it not touches the bounds of the maze or the sanke body.



Chess II: Agressive Pawns And A King

Expand your Chess game towards "hitting" and add the actor type "King"..





Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/

...back from North Pole.

Midimaster

#16
Lesson XVI: More Maze Elements
Today we will add a couple of new actors and elements to our maze. Enemies, hidden doors and their keys and food for the player. Also we need a bomb!

Constants instead of number

Before we add a lot of different elements in our maze we should stop working with numbers. The code is more readable when we use symbolic CONSTANTs:

Let us have a look on our code part, where we decided what we will draw:
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
If Check(x,y , 1)
...
If Check(x,y , 2)
...
If Check(x,y , 4)
...
If Check(x,y , 8)
...
Next
Next


A third person cannot understand what this code does and which purpose has the 1, the 2 the 4 the 8 or the 11! How much better would be a code like this:
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
If Check(x,y , TOP_WALL)
...
If Check(x,y , LEFT_WALL)
...
If Check(x,y , PLAYER)
...
If Check(x,y , GOLD)
...
Next
Next

Even we have an advantage in reading after our number of elements increased at the end of the day.
For more elements we now define additional variables. But we already know that their value will never change. We call this type CONSTANTs:

Code (BlitzMax) Select
Const TOP_WALL:Int   = 1
Const LEFT_WALL:Int  = 2
Const THE_PLAYER:Int = 4
Const GOLD:Int       = 8
Const DOOR:Int       = 16
Const KEY:Int        = 32
Const BOMB:Int       = 64
Const COOKIE:Int     =128



Const GOLD:Int=8 defines a symbolic constant. You use it instead of a number. Constants are often written with capital letters


and you see a first effect here. Instead of writing...
For Local I:Int=1 To 10
Add i, 1 , 1
Add i,11 , 1
Next
For Local I:Int=1 To 10
Add 1, i , 2
Add 11,i , 2
Next

...we now can write...
Code (BlitzMax) Select
For Local I:Int=1 To 10
Add i, 1 , TOP_WALL
Add i, 11 , TOP_WALL
Next
For Local I:Int=1 To 10
Add 1, i , LEFT_WALL
Add 11 , i , LEFT_WALL
Next



Current state of our code:


Code (BlitzMax) Select
SuperStrict
Graphics 800,650

Const TOP_WALL:Int   = 1
Const LEFT_WALL:Int  = 2
Const THE_PLAYER:Int = 4
Const GOLD:Int       = 8
Const DOOR:Int       = 16
Const KEY:Int        = 32
Const BOMB:Int       = 64
Const COOKIE:Int     =128

Global P_Image:TImage = LoadImage("player.png")

Global Board:Int[12,12]

For Local I:Int=1 To 10
Add i, 1 , TOP_WALL
Add i, 11 , TOP_WALL
Next

For Local I:Int=1 To 10
Add 1, i , LEFT_WALL
Add 11, i , LEFT_WALL
Next

For Local i:Int=0 To 40
Add  Rand(1,10) , Rand(1,10)  , TOP_WALL
Add  Rand(1,10) , Rand(1,10)  , LEFT_WALL
Next


Add 5,5 , THE_PLAYER
Add 6,8 , GOLD


Global Size:Int=50
Repeat
Cls
For Local y:Int= 1 To 11
For Local x:Int= 1 To 11

If Check(x,y ,  TOP_WALL)
SetColor 105,80,65
DrawRect x*size , y*size-3, size, 6
EndIf

If Check(x,y , LEFT_WALL)
SetColor 105,80,65
DrawRect x*size-3 , y*size, 6, size
EndIf

If Check(x,y , THE_PLAYER)
SetColor 255,255,255
DrawImage P_Image, x*size , y*size
EndIf

If Check(x,y , GOLD)
SetColor 200,150,0
DrawOval x*size+10 , y*size+10,20,20
EndIf
Next
Next
CheckPlayer
Flip
Until AppTerminate()


Function CheckPlayer()
If KeyHit(KEY_LEFT)
FindAndMovePlayer(-1,0)
ElseIf KeyHit(KEY_RIGHT)
FindAndMovePlayer(+1,0)
ElseIf KeyHit(KEY_UP)
FindAndMovePlayer(0,-1)
ElseIf KeyHit(KEY_DOWN)
FindAndMovePlayer(0,+1)
EndIf
End Function


Function FindAndMovePlayer(moveX:Int, moveY:Int)
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
If Check (x,y, THE_PLAYER)
If     moveX=-1
If Check(   x,y   ,LEFT_WALL ) Return
ElseIf moveX=1
If Check( x+1,y   ,LEFT_WALL ) Return
ElseIf moveY=-1
If Check(   x,y   ,TOP_WALL ) Return
ElseIf MoveY=1
If Check(   x,y+1 ,TOP_WALL ) Return
EndIf
Add x+moveX ,y+moveY , THE_PLAYER
Remove x,y, THE_PLAYER
Return
EndIf
Next
Next
End Function



Function Add(X:Int, Y:Int, element:Int)
Board[x,y] = Board[x,y] | element
End Function


Function Remove(X:Int, Y:Int, element:Int)
Board[x,y]=Board[x,y] &~ element
End Function


Function Check:Int(X:Int, Y:Int, element:Int)
Return (Board[x,y] & element) = element
End Function 



Some Cookies = Energie for the player

Let us define a new game rule:

The player consums energy when running through the maze. Lets say each step costs 1 energy. So he periodically needs cookies to refuel his energy level. Eating a cookie brings 10 energie.


For each new element we will add now we need always the same seven changes in our code:


Change 1: Define a Constant:

Code (BlitzMax) Select
Const COOKIE:Int     =128
So we can code with a symbolic constant instead of the number 128


Change 2: Define a related Player's variable:

Code (BlitzMax) Select
Global P_Energy:Int
In this variable we will add 10, when the player eats a cookie, and substract 1 on each step the player makes.


Change 3: Spread some random cookies into the maze:

Code (BlitzMax) Select
For Local i:Int=0 To 10
Add  Rand(1,10) , Rand(1,10)  , COOKIE
Next

We code this below the wall creation code


Change 4: Display the element

Code (BlitzMax) Select
...
For Local y:Int= 1 To 11
For Local x:Int= 1 To 11
....
If Check(x,y , COOKIE)
SetColor 100,50,0
DrawOval x*size+20 , y*size+20,7,7
EndIf
....

In the Maze-Drawing loop we add a branch for cookies


Change 5: Display the player's state:

Code (BlitzMax) Select
...
DrawText "ENERGY=" + P_Energy,100,5
FLIP

This displays how much energy the player still has. The best place to draw this is shortly before the FLIP


Change 6: Define what happens when the player "finds" any element at his new position:

Code (BlitzMax) Select
Function CheckNewPlayerPosition(X:Int, Y:Int)
If Check(x,y , COOKIE )
Remove x,y, COOKIE
P_Energy = P_Energy+10
EndIf 
Return

Therefore we add a new function CheckNewPlayerPosition() which we call each time the player moved.


Change 7:  Substract the energie

Code (BlitzMax) Select
Function FindAndMovePlayer(moveX:Int, moveY:Int)
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
...
P_Energy = P_Energy-1     '                    <------ HERE
If P_Energy<1 Return        '                    <----- stop the player when empty

Add x+moveX ,y+moveY , THE_PLAYER
Remove x,y, THE_PLAYER
CheckNewPlayerPosition ( x+moveX ,y+moveY)   '  <-------- NEW AFTER-MOVE-FUNCTION
Return

...

The best point will be where we already exchanged the the player's field. This is also the best point for call the CheckNewPlayerPosition() function


So our new version looks like this:


Code (BlitzMax) Select
SuperStrict
Graphics 800,650

Const TOP_WALL:Int   = 1
Const LEFT_WALL:Int  = 2
Const THE_PLAYER:Int = 4
Const GOLD:Int       = 8
Const DOOR:Int       = 16
Const KEY:Int        = 32
Const BOMB:Int       = 64
Const COOKIE:Int     =128

Global P_Image:TImage = LoadImage("player.png")
Global P_Energy:Int  = 10

Global Board:Int[12,12]

For Local I:Int=1 To 10
Add i, 1 , TOP_WALL
Add i, 11 , TOP_WALL
Next

For Local I:Int=1 To 10
Add 1, i , LEFT_WALL
Add 11, i , LEFT_WALL
Next

For Local i:Int=0 To 40
Add  Rand(1,10) , Rand(1,10)  , TOP_WALL
Add  Rand(1,10) , Rand(1,10)  , LEFT_WALL
Next

For Local i:Int=0 To 10
Add  Rand(1,10) , Rand(1,10)  , COOKIE
Next

Add 5,5 , THE_PLAYER
Add 6,8 , GOLD


Global Size:Int=50
Repeat
Cls
For Local y:Int= 1 To 11
For Local x:Int= 1 To 11

If Check(x,y ,  TOP_WALL)
SetColor 105,80,65
DrawRect x*size , y*size-3, size, 6
EndIf

If Check(x,y , LEFT_WALL)
SetColor 105,80,65
DrawRect x*size-3 , y*size, 6, size
EndIf

If Check(x,y , THE_PLAYER)
SetColor 255,255,255
DrawImage P_Image, x*size , y*size
EndIf

If Check(x,y , GOLD)
SetColor 200,150,0
DrawOval x*size+10 , y*size+10,20,20
EndIf

If Check(x,y , COOKIE)
SetColor 100,50,0
DrawOval x*size+20 , y*size+20,7,7
EndIf

Next
Next
CheckPlayer
SetColor 255,255,0
DrawText "ENERGY=" + P_Energy,100,5
If P_Energy<1
DrawText " G A M E   O V E R   ! ! ! ! ! ! ", 300,5
EndIf
Flip
Until AppTerminate()


Function CheckPlayer()
If KeyHit(KEY_LEFT)
FindAndMovePlayer(-1,0)
ElseIf KeyHit(KEY_RIGHT)
FindAndMovePlayer(+1,0)
ElseIf KeyHit(KEY_UP)
FindAndMovePlayer(0,-1)
ElseIf KeyHit(KEY_DOWN)
FindAndMovePlayer(0,+1)
EndIf
End Function


Function FindAndMovePlayer(moveX:Int, moveY:Int)
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
If Check (x,y, THE_PLAYER)
If     moveX=-1
If Check(   x,y   ,LEFT_WALL ) Return
ElseIf moveX=1
If Check( x+1,y   ,LEFT_WALL ) Return
ElseIf moveY=-1
If Check(   x,y   ,TOP_WALL ) Return
ElseIf MoveY=1
If Check(   x,y+1 ,TOP_WALL ) Return
EndIf
P_Energy = P_Energy-1
If P_Energy<1 Return

Add x+moveX ,y+moveY , THE_PLAYER
Remove x,y, THE_PLAYER
CheckNewPlayerPosition ( x+moveX ,y+moveY)
Return
EndIf
Next
Next
End Function


Function CheckNewPlayerPosition(X:Int, Y:Int)
If Check(x,y , COOKIE )
Remove x,y, COOKIE
P_Energy=P_Energy+10
EndIf 
End Function


Function Add(X:Int, Y:Int, element:Int)
Board[x,y] = Board[x,y] | element
End Function


Function Remove(X:Int, Y:Int, element:Int)
Board[x,y]=Board[x,y] &~ element
End Function


Function Check:Int(X:Int, Y:Int, element:Int)
Return (Board[x,y] & element) = element
End Function




Challenge I : PacMan

Write a pacman game. Player is walking through a maze and eating pills, while ghoosts are hunting him.



Challenge II : Expand Chess

Add Rooks and Bishops and their functionality to your chess game.



Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/

...back from North Pole.

Midimaster

#17
Lesson XVII: More Maze Elements II

Doors and Keys

New game rule:
The maze has closed rooms with a door to enter them. The player cannot open the door until he found a key.


So today we have to add two new game elements and one new player's variable which can transport a key until we need it.

This are our seven steps:


Change 1: Define the Constants:

Code (BlitzMax) Select
Const DOOR:Int       = 16
Const KEY:Int        = 32



Change 2: Define a related Player's variable:

Code (BlitzMax) Select
Global P_Keys:Int     =  0
Global Key_Image:TImage=LoadImage("key.png")

additional we need to load the image of a key for displaying it in the maze (see attachment)

Change 3: Bring a door and a key into the maze:

Code (BlitzMax) Select
Add 4,8 , KEY
Add 11,4 , DOOR



Change 4: Display the elements

Code (BlitzMax) Select
If Check(x,y , DOOR)
SetColor 105,180,65
DrawRect x*size-3 , y*size, 6, size
EndIf

If Check(x,y , KEY)
SetColor 255,255,255
DrawImage Key_Image, x*size , y*size
EndIf



Change 5: Display the player's state:

Code (BlitzMax) Select
DrawText "YOU HAVE " + P_Keys + " KEY ", 200,5



Change 6: Define what happens when the player "finds" the elements:

Code (BlitzMax) Select
Function CheckNewPlayerPosition(X:Int, Y:Int)
...
If Check(x,y , KEY )
Remove x,y, KEY
P_Keys = P_Keys+1
EndIf 

If Check(x+1,y , DOOR )
If P_Keys>0
Remove x+1,y, DOOR
Remove x+1,y, LEFT_WALL
P_Keys = P_Keys-1
EndIf
EndIf 
End Function

dont forget to remove also the wall in this field!

Change 7: already done in change 6

Additional Change 8: leave the level when the player exits the maze

Code (BlitzMax) Select
Function CheckNewPlayerPosition(X:Int, Y:Int)
If x=11 End
...
End Function

The player can only reach column 11, when he stepped through the exit door. So "beeing on 11" is a good signal for "beeing ready".


Our Wall-System

Some words about our wall system... It looks like we have left and right walls and also top and bottom walls. But we defined only LEFT_WALL and TOP_WALL. So, how did we get the right walls?

The trueth is....

The right walls are in truth LEFT_WALLs of the right neighbor field. Also the bottom walls, they are the TOP_WALL of the lower neighbor field.

The neighbor walls next to the players at X,Y can be found at:
left wall     :  Board[x,y]
top wall     :  Board[x,y]
right wall   :  Board[x+1,y]
bottom wall:  Board[x,y+1]


This is the reason why we check the neighbor walls like this:
left wall   : Check(  x,y  ,LEFT_WALL)
top wall    : Check(  x,y  ,TOP_WALL )
right wall  : Check(1+x,y  ,LEFT_WALL)
bottom wall : Check(  x,y+1,TOP_WALL )


These neighbor-checks are always a reason for runtime errors. As long as the player is not at the bounds of our maze everything works fine. But what happen if we put him at one of the the most right fields [11,0] to [11,11]?

The position is okay. The player is still inside the maze. but if we would now check the neighbors, we would also check the right neighbor at [12,...] and this fires a runtime error, because we are out of our array-dimensions.

This means we have to prevent, that the player can never reach the rows 0 and 11 and also never the columns 0 and 11. Or in other words, we have to define  mazes 2 fields bigger than we want to use them. That is the reason, why we only play inside the fields 1 to 10, while our maze is dimensioned from 0 to 11. With this security buffers we can asks all neighbors of all walkable field.


Random without Random

If you start the game again and again you get each time completely different walls in the game. This is because we use the RAND() function for creating the walls. Also your end user would see a complete unknown situation. This is funny, but can cause a unplayable game. Think about a situation the the player is (randomly) sourrounded by 4 walls! He cannot walk.

As a game designer you want to present the user a "defined" random-build maze, where you ensure, that it is 100% playable. Therefore we have the command SeedRnd()

SeedRnd() Seed:Int repeats a random-sequence defined by a seed-value.
RndSeed:Int() shows you the Seed from the current random-sequence.

As a developer you do not define a seed, but print the current seed. If you like the resulting maze, you note down the RndSeed() value and use this for the users version:

Developers version:

Code (BlitzMax) Select
SuperStrict
Graphics 800,650
' SeedRnd 34567
Print "This Seed=" + RndSeed()
....



Users version:

Code (BlitzMax) Select
SuperStrict
Graphics 800,650
SeedRnd 34567
Print "This Seed=" + RndSeed()
....


This way I found out, that 34567 always produces a nice looking and playable maze.




Challenge I : Add A Bomb

New game rule:
The player can collect a bomb. With a bomb he can blast away upto 4 wall next to him.

This needs exactly the same 7 steps like "key". Additional you need to define a KeyHit(), where the user can "throw" the bomb.


Challenge II : Tetris

Write a maze game, where 4-block-objects fall from the top unil they meet other objects. The user can turn and move the objects. Ff a row at he bottom gets "full" it disappears.   


Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/




...back from North Pole.

Midimaster

#18
Lesson XVIII: Enemies will find you

Today we have our last chapter of mazes. We define enemies and how they search the player.

Enemies

we follow the same 7 step procedure to establish an element "enemy":


Change 1: Define the Constants:

Code (BlitzMax) Select
Const ENEMY:Int       = 256
.....
Global Enemy_Image:TImage=LoadImage("enemy.png")


Of course we have to add also a image of an enemy.


Change 2: Define a related Player's variable:

not necessary, because if the enemy finds the player the player is dead immediately.


Change 3: Bring some enemies into the maze:

Code (BlitzMax) Select
Add 1,1 , ENEMY
Add 1,10 , ENEMY
Add 10,10 , ENEMY



Change 4: Display the elements


Code (BlitzMax) Select
If Check(x,y , ENEMY)
SetColor 255,255,255
DrawImage Enemy_Image, x*size , y*size
EndIf




Change 5: Display the player's state:

not necessary, because if the enemy finds the player the player is dead immediately.



Change 6: Define what happens when the player "meets" the elements:

Code (BlitzMax) Select
Function CheckNewPlayerPosition(X:Int, Y:Int)
...
If Check(x,y , ENEMY )
P_Energy=0
EndIf 
End Function


This is easy, we set the players energy to 0 and the player is dead.



Additional Change 8: Move the enemies

This is a the only new topic. The best point to move the enemies is once a FLIP in the main loop. We add a new function FindAndMoveEnemies() in the line before CheckPlayer

Code (BlitzMax) Select
...
Next
Next
FindAndMoveEnemies()   '   <------ HERE
CheckPlayer
SetColor 255,255,0
...
Flip
Until AppTerminate() 



We can immediately add this function. The fist part allows ony to move the enemies every 500msec. The second part scan the maze to find all enemies.
Code (BlitzMax) Select
Function FindAndMoveEnemies()
Global EnemyTime:Int
If EnemyTime>MilliSecs() Return
EnemyTime=MilliSecs()+500

For Local y:Int= 0 To 10
For Local x:Int= 0 To 10
If Check (x,y,ENEMY)
MoveEnemy(x,y)
EndIf
Next
Next
End Function   


If you add a GLOBAL variable inside a function it works like a GLOBAL variable, but it is only known inside the function. We use this to store a timestamp for the enemies' intervalls.

Current version of the game:

Code (BlitzMax) Select
SuperStrict
Graphics 800,650
SeedRnd 34567
Print "This Seed=" + RndSeed()

Const TOP_WALL:Int   = 1
Const LEFT_WALL:Int  = 2
Const THE_PLAYER:Int = 4
Const GOLD:Int       = 8
Const DOOR:Int       = 16
Const KEY:Int        = 32
Const BOMB:Int       = 64
Const COOKIE:Int     =128
Const ENEMY:Int       = 256

Global P_Image:TImage = LoadImage("player.png")
Global P_Energy:Int  = 10
Global P_Keys:Int     =  0
Global Key_Image:TImage=LoadImage("key.png")
Global bomb_Image:TImage=LoadImage("bomb.png")
Global Enemy_Image:TImage=LoadImage("enemy.png")

Global Board:Int[12,12]

For Local I:Int=1 To 10
Add i, 1 , TOP_WALL
Add i, 11 , TOP_WALL
Next

For Local I:Int=1 To 10
Add 1, i , LEFT_WALL
Add 11, i , LEFT_WALL
Next


For Local i:Int=0 To 20
Add  Rand(1,10) , Rand(1,10)  , TOP_WALL
Add  Rand(1,10) , Rand(1,10)  , LEFT_WALL
Next

For Local i:Int=0 To 10
Add  Rand(1,10) , Rand(1,10)  , COOKIE
Next

Add 5,5 , THE_PLAYER
Add 6,8 , GOLD
Add 4,8 , KEY
Add 11,4 , DOOR
Add 1,1 , ENEMY
Add 1,10 , ENEMY
Add 10,10 , ENEMY


Global Size:Int=50
Repeat
Cls
For Local y:Int= 1 To 11
For Local x:Int= 1 To 11

If Check(x,y ,  TOP_WALL)
SetColor 105,80,65
DrawRect x*size , y*size-3, size, 6
EndIf

If Check(x,y , LEFT_WALL)
SetColor 105,80,65
DrawRect x*size-3 , y*size, 6, size
EndIf

If Check(x,y , THE_PLAYER)
SetColor 255,255,255
DrawImage P_Image, x*size , y*size
EndIf

If Check(x,y , GOLD)
SetColor 200,150,0
DrawOval x*size+10 , y*size+10,20,20
EndIf

If Check(x,y , COOKIE)
SetColor 100,50,0
DrawOval x*size+20 , y*size+20,7,7
EndIf

If Check(x,y , DOOR)
SetColor 105,180,65
DrawRect x*size-3 , y*size, 6, size
EndIf
If Check(x,y , KEY)
SetColor 255,255,255
DrawImage key_Image, x*size , y*size
EndIf

If Check(x,y , ENEMY)
SetColor 255,255,255
DrawImage Enemy_Image, x*size , y*size
EndIf
Next
Next
FindAndMoveEnemies()
CheckPlayer
SetColor 255,255,0
DrawText "ENERGY=" + P_Energy,100,5
If P_Energy<1
DrawText " G A M E   O V E R   ! ! ! ! ! ! ", 350,5
SetClsColor 150,0,0
EndIf
DrawText "YOU HAVE " + P_Keys + " KEY ", 200,5
Flip
Until AppTerminate()


Function CheckPlayer()
If KeyHit(KEY_LEFT)
FindAndMovePlayer(-1,0)
ElseIf KeyHit(KEY_RIGHT)
FindAndMovePlayer(+1,0)
ElseIf KeyHit(KEY_UP)
FindAndMovePlayer(0,-1)
ElseIf KeyHit(KEY_DOWN)
FindAndMovePlayer(0,+1)
EndIf
End Function


Function FindAndMovePlayer(moveX:Int, moveY:Int)
For Local y:Int= 1 To 10
For Local x:Int= 1 To 10
If Check (x,y,THE_PLAYER)
If     moveX=-1
If Check(   x,y   ,LEFT_WALL ) Return
ElseIf moveX=1
If Check( x+1,y   ,LEFT_WALL ) Return
ElseIf moveY=-1
If Check(   x,y   ,TOP_WALL ) Return
ElseIf MoveY=1
If Check(   x,y+1 ,TOP_WALL ) Return
EndIf
P_Energy = P_Energy-1
If P_Energy<1 Return

Add x+moveX ,y+moveY , THE_PLAYER
Remove x,y, THE_PLAYER
CheckNewPlayerPosition ( x+moveX ,y+moveY)
Return
EndIf
Next
Next
End Function



Function FindAndMoveEnemies()
Global EnemyTime:Int
If EnemyTime>MilliSecs() Return
EnemyTime=MilliSecs()+500

For Local y:Int= 0 To 10
For Local x:Int= 0 To 10
If Check (x,y,ENEMY)
MoveEnemy(x,y)
EndIf
Next
Next
End Function   


Function MoveEnemy(X:Int, y:Int)
'part I decision:
Local direction:Int=Rand(1,4)

'part II check possible?
If     direction=1
If Check(   x,y   ,LEFT_WALL ) Return
Add x-1 ,y , ENEMY
Remove x,y, ENEMY

ElseIf direction=2
If Check( x+1,y   ,LEFT_WALL ) Return
Add x+1 ,y , ENEMY
Remove x,y, ENEMY
ElseIf direction=3
If Check(   x,y   ,TOP_WALL ) Return
Add x ,y-1 , ENEMY
Remove x,y, ENEMY
ElseIf direction=4
If Check(   x,y+1 ,TOP_WALL ) Return
Add x ,y+1 , ENEMY
Remove x,y, ENEMY
EndIf
If check(p_X , p_Y, ENEMY)=True
P_Energy=0
EndIf 
End Function


Function CheckNewPlayerPosition(X:Int, Y:Int)
If x=11 End
If Check(x,y , COOKIE )
Remove x,y, COOKIE
P_Energy = P_Energy+10
EndIf

If Check(x,y , KEY )
Remove x,y, KEY
P_Keys = P_Keys+1
EndIf 
If Check(x+1,y , DOOR )
If P_Keys>0
Print p_keys
Remove x+1,y, DOOR
Remove x+1,y, LEFT_WALL
P_Keys = P_Keys-1
EndIf
EndIf

If Check(x,y , ENEMY )
P_Energy=0
EndIf 
End Function


Function Add(X:Int, Y:Int, element:Int)
Board[x,y] = Board[x,y] | element
End Function


Function Remove(X:Int, Y:Int, element:Int)
Board[x,y]=Board[x,y] &~ element
End Function


Function Check:Int(X:Int, Y:Int, element:Int)
Return (Board[x,y] & element) = element
End Function   






Moving an enemy

By Random

The easy way to move an enemy is to do it by random. This moves will look very uncoordinated.
We need only one code line:
Code (BlitzMax) Select
Function MoveEnemy(X:Int, y:Int)
'part I decision:
Local direction:Int=Rand(1,4)

'part II check possible?
If     direction=1
If Check(   x,y   ,LEFT_WALL ) Return
Add x-1 ,y , ENEMY
Remove x,y, ENEMY

ElseIf direction=2
If Check( x+1,y   ,LEFT_WALL ) Return
Add x+1 ,y , ENEMY
Remove x,y, ENEMY
ElseIf direction=3
If Check(   x,y   ,TOP_WALL ) Return
Add x ,y-1 , ENEMY
Remove x,y, ENEMY
ElseIf direction=4
If Check(   x,y+1 ,TOP_WALL ) Return
Add x ,y+1 , ENEMY
Remove x,y, ENEMY
EndIf
If check(p_X , p_Y, ENEMY)=True
P_Energy=0
EndIf
End Function

the second part is the check whether this new field would be free. It will be the same for all following moving strategies


Move by Finding the player

first step is to store the players last position in two GLOBAL variables: p_x:int and p_y:int . We can do this in the function CheckNewPlayerPosition():
Code (BlitzMax) Select
Function CheckNewPlayerPosition(X:Int, Y:Int)
P_X = X  '   <---- NEW
P_Y = Y  '   <---- NEW
If x=11 End
...

(do not forget to define the two new variables at the top of our code)


Now the enemies can prefer the direction to the player:

Code (BlitzMax) Select
'part I decision:
Local direction:Int
Local random:Int=Rand(1,4)
If Random=1
If p_X>X Then direction=2
ElseIf Random=2
If p_X<X Then direction=1
ElseIf Random=3
If p_Y>Y Then direction=4
ElseIf Random=4
If p_Y<Y Then direction=3
EndIf

'part II check possible?
..... 

A random part selects to check one of the four possible constellations and then decide to move in this direction. The resulting moves already make the impression of dangerous enemies. but if the move into to dead end corridor in our maze they are lost.



Combination of Random and Search


This make the enemies moving "crazy". Sometime they are straight, sometime they do stupid things. The random produces 8 states, only 4 are intelligent and may change the random direction, the other 4 keep the random direction.

Code (BlitzMax) Select
Function MoveEnemy(X:Int, y:Int)
'part I decision:
Local direction:Int = Rand(1,4)
Local random:Int = Rand(1,8)
If Random=1
If p_X>X Then direction=2
ElseIf Random=2
If p_X<X Then direction=1
ElseIf Random=3
If p_Y>Y Then direction=4
ElseIf Random=4
If p_Y<Y Then direction=3
EndIf

'part II check possible?
...




Challenge I: PacMan with Intelligence

Try to improve your PacMan game. Add a finding algorithm for the ghoosts.


Challenge II: Maze Editor.

Try to write an editor where we can "build" new mazes by secting a element typ, then move it into the maze. [/b]


Challenge III: Pseudo 3D Maze.

Try to write a algorithm that displays a active "wall" field in a maze as a 3D-Cube. Do not use a 3D-Api, but teach yourself the command DrawPoly()




Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/

...back from North Pole.

Midimaster

#19
Lesson XIX: User Text Input: Keycodes ASCIIs &  Strings

Today we will have a look on possibilties to enter texts or numbers by the user. Or to enter text via files. Also saving text to files will be our theme. Therefor we need more knowledge about STRINGS.


KeyCodes Or ASCII?

The BlitzMax keyboard related commands can deliver two different things. Keycodes or ASCII. Each key-button has a number and we can ask it. We did this already with the commands KEYHIT() and KEYDOWN(). That is called the KeyCode(). They are often used in games to manage the game features.

On the other side you know, that on every key there is printed more than one letter or sign. With a combination of the key together with SHIFT or ALT or CTRL you can access them. This approach delivers ASCII. We use them to enter names and write text.


The Keyboard sends KeyCodes

As we already know we can check Keyboard keys with KEYHIT(KEY_CODE). This function returns TRUE or FALSE depending whether the users pressed the key or not. But what happens if we do not check only one KeyCode but all 255 avaiable?


Code (BlitzMax) Select
SuperStrict
Graphics 800,600

Repeat
Cls
Local value:Int = CheckAllKeys()

If value>0
Print "Key pressed = " + value
EndIf

Flip
Until AppTerminate()

Function CheckAllKeys:Int()
For Local i:Int=1 To 255
If KeyHit(i) Return i
Next
Return 0
End Function


This function continously returns zero if nothing happens.

But what happens if the user presses any key? As it checks all 255 theoretic avaiable KeyCodes in a loop it will "find" the KeyCode that report TRUE. In this moment the function reports the i,which caused the success and so we discover the KeyCode of this user key. Test it and play with it.




The Keyboard sends ASCII

To return the letters printed on a keyboard key we use the function GetChar(). Now weget back three different values for the same key, depending whether we pressed SHIFT or ALT or CTRL in combination with this key:

Code (BlitzMax) Select
SuperStrict
Graphics 800,600
Global letter:Int
Repeat
Cls
Local value:Int = GetChar()

If value>0
letter=value
EndIf
DrawText "ASCII = " + letter , 100 , 100
Flip
Until AppTerminate()




This function continously returns zero if nothing happens.

But what happens if the user presses any key? Then it returns the ASCII-code related to the key. Test it and play with it.

GetChar()  returns the ASCII of a key or zero if nothing happened.


ASCII-Codes

The functions GetChar() returns numbers from 1 to 255. This are respresentatives for the letters. We can use those numbers directly in STRINGS.


Here are some important ASCIIs:

ASCI       LETTERS           Description
--------------------------------------------------
65 -  90   A B C .. X Y Z    from 65 to 90 are all capital letter of the ABC
97 - 122   a b c .. x y z    from 97 to 122 are all lower case of the ABC
48 -  57   0 1 2 .. 7 8 9    the 10 digits from  "0" to "9"





Write this extended code and save it to a project folder.

Code (BlitzMax) Select
SuperStrict
Graphics 800,600

Global MyFont:TimageFont=LoadImageFont("arial.ttf",140)
SetImageFont MyFont
Global letter:Int
Repeat
Cls
Local value:Int = GetChar()

If value>0
Print "Key pressed = " + value + " -> character:"  +Chr(value)
letter=value
EndIf
DrawText Chr(letter),100,100
Flip
Until AppTerminate()




Chr(value:Int)  converts a ASCII-number into a String.

MyFont:TimageFont  TImageFont is a variable type for Fonts in BlitzMax

LoadImageFont("arial.ttf",40)  Loads a TrueTypeFont from your project folder into a variable TImageFont. The second parameter defines the size of the letters on the screen.

SetImageFont MyFont  Forces DrawText() now to use this font. You can change this as often as you want.


A Better Font for DrawText

A "Font" is an image collection of letters to draw them with DrawText(). The default BlitzMax Font contains only a reduced number of letters(signs). So this is the best opportunity to learn how to use better fonts.

You already have a lot of Fonts on your computer. You can use them in BlitzMax by copying them into your project folder. All Windows fonts are in "C:\Windows\Fonts\". For BlitzMax we can use only True-Type-Fonts, you recognize them by the extension ".TTF" Be careful to really "copy" your font and not "move" it!  For our todays example we need the Font arial.ttf.



Strings Are Arrays?
Perhaps you think that STRINGS are harmless variable type? But STRINGs can more: STRINGS can have the size of one byte upto millions of byte. So I would more compare them with ARRAYS. And really!!! A STRING is more like an array than like a variable.

String do not save "words", but all letters (of our word) as ASCII values in an array. Try this:
Code (BlitzMax) Select
Global Word:String= "ABCDEFGHIJ"

For Local i:Int=0 Until Word.Length
Print Word[i]
Next


This word[0] with its brackets look like an array? The numbers you will see are the ASCII-Numbers of the letters (or as we say " of the characters")

new:
FOR I... UNTIL array.length
finishes the loop 1 step before it reaches the value of array.Length. An array of 10 elements contains the elements 0...9. so we need to stop a loop not at 10 but already at 9! with a FOR..TO..loop we ould run upto 10!

word[0] is called a SLICE. It a way to handle STRINGS with brackets like ARRAYS.

Try this:

Code (BlitzMax) Select
Global Word:String= "ABCDEFGHIJ"
Word[2]=57
Print word


It cuts our the "C" (third letter: [2] ! ) and replaces it by a "9" (ASCII=57)


Challenge I: Write your name

Write an app that displays a complete name in big letters on the screen. When the user presses RETURN it start new with an empty screen


Challenge II: Diffrent text size

Write an app that displays two texts in different sizes and color. One text should rotate.


Challenge II: Write a calculator app

Write an app that displays a calculator. The user can enter the number and + - * and / and on ENTER it displays the result





Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/

...back from North Pole.

iWasAdam

QuoteThe keyboards can deliver two different things. Keycodes or ASCII.

NO Completely wrong and factually wrong too!


Keycodes are the internal way of interfacing with the event system - it sends codes and these are then translated by the language into keycodes
These keycodes are not fixed - so they could be anything. but are usually referenced via something sensible. E.G.

KEY_A should generally equal A.

But. This is also not strictly true as A and a are both KEY_A. to get the other variants you have to track (or the language tracks for you) things like shift presses, etc.

The keyboard interface itself just sends a code - this is sent into the event system and then the language translates that into a keycode!

----------------

And next complete rubbish spouted by the author. The keyboard DOES NOT SEND OR HAVE ANYTHING TO DO WITH ASCII!

Yes GetChar() will convert stuff to ASCII. But this is a helper that has been coded into blitz. and has NOTHING to do with ASCII

What is ASCII?
ASCII codes represent text in computers - it is an extension of Telegraph codes which was 7bit based and not uniform between manufacturers.
ASCII was first proposed in 1961, first released in 1963 and finalized in 1968.

ASCII was developed for teletype systems and retains all of the inbuild system codes - delete, enter, etc in the first 31 characters. The rest filling the alpha and numeric characters that were required. IBM further Standarized its mainframe system to the ANSI ASCII standard in 1965.

Why is this important? because it tells you that ASCII and keyboards (although related) don't have any connection. ASCII was and is used a core character set. that is standard and can be used by manufacturers to make teletype systems and mini computers use the same basic characters. these fit into 7bits with a single bit parity.

The key thing here is IBM. They are the standard that everyone followed. They stuck with the ANSI variation of ASCII and so ASCII became the defacto standard.


This brings us to characters and strings. A character (Char) is an 8bit number generally relating to the ASCII standard. Although fonts relate differently as the upper 127 characters can be anything - graphics, other needed character for different languages etc.

So 32 = SPACE, 65 = A, 66 = B, etc

a string is defined as a series of Char(s)
Hence you can think of a string as an array of single Char(s) too.

But THE KEYBOARD DOES NOT SEND ASCII - EVER!!!!!

-------------------------------------

Telling people - especially someone you think is attempting to learn these this is not good. It's not how these things work and it only makes their learning harder as they will have to figure out why something is not working they way they were told.

This is VERY POOR TEACHING!!!!

iWasAdam

ALSO:
stuff like ctrl, shift/option, etc will have to be parsed by the end user and they will have to decide how and what to do with this information.

Remember that different keyboards will give completely results - further showing the real disconnect from Keycodes and ASCII.

iWasAdam

Another caveat for you and your 'learners'.

blitz is designed as an even based system, with general class/app based frame work.

To teach any new person about using globals as the core variable is not just poor teaching. it wouldn't be accepted in any school or work environment.

You might be better to use a simpler basic to teach beginner level stuff. and use Blitz to actually teach people how to use that properly?


Midimaster

#23
Lesson XX: Text-Files and String Manipualtion

Today you will learn how to load and save informations to the hard disc. This is often used to get words and sentences into your game. And a important aspect is to define different levels and get the informations from a file. This has the advantage, that you can write "levels" directlty as a txt-file with a simple txt-editor.

A TXT-File

Txt-Files are clear readable texts on a hard disk. you can read, write an manipulate them directly with a text-editor. Afterward your app tries to open them and set variables depending on what you have written in the txt-file.

You can use every external Editor as long as it produces file with the extension .TXT. A app like WORD would not be usefull, because WORD saves additional styling informations into the file. But we need pure ASCII-Code.

You can directly use the BlitzMax IDE to write such a text file. Open a new code-window and write:
hello world
But now save the content as "test.txt" into your project folder.

Open another code-window and write this app:
Code (BlitzMax) Select
SuperStrict

Global All:String = LoadText("test.txt")

Print All


LoadText:String( FileName:String) loads a txt-file and returns the complete content as one string into a variable.

Now change our text to:
Hello World
This is line two
And this another one

Ignore that fact, that the IDE highlights the word "And" and save the file as test.txt.

Now re-start our app an observe that the variable All contain all lines of our text in one variable.


Cut text into different variables

The variable type STRING knows various internal functions to manipulate strings. Those internal functions are only avaiable for strings, we call them Methods. You call Methods by adding a dot to your variable name followed by the method name:

Code (BlitzMax) Select
... = All.Split("?")

The method Split:String[] (Separator) splits the content of a STRING into a STRING array. Each element of the array contains only a part of the original text All. Here the method splits the text every time it finds a ? letter.


But we do it this way:

Code (BlitzMax) Select
SuperStrict

Global All:String$ = LoadText("test.txt")
Global word:String[] = All.Split(" ")  '  <---- there is a SPACE between the quotation marks

Print word[6]

This splits the text into single strings. Because the separator is a SPACE letter, we get single words as result.


Cut text into lines

We can use this Method Split() to split a text into lines. We detect "a new line" where two special ASCIIs are in the text. BlitzMax know them as "~r" and "~n"

Code (BlitzMax) Select
SuperStrict

Global All:String = LoadText("test.txt")
Global Line:String[] = All.Split("~r~n")

Print ">" + Line[1] +"<"

If Line[1] = "This is line two" Then Print "Is the same"


~r (RETURN) and ~n (NEWLINE) are BlitzMax-Shortcuts for the ASCIIs 13 and 10, which are also known as CRLF (Carriage Return + Line Feeld)


Now let us create our first INI-file:

Ini-files are often used to initialize a game with parameters, get values and sentences into your game. A common aspect is to define different levels and fetch the informations from a file during the game.

Save this new plain text as "Ini.txt"

Name=Peter
Age=25
Health=3.235
' now a empty line:

GameGadgets=4|123|0|4|-7


This is a typical INI-file. Our Ini.txt contains different problematics: A String like Peter, values like 25, comments like ' now a empty line: and a real empty line.


To open it we do the same like before. Separate the lines:

Code (BlitzMax) Select
SuperStrict

Global All:String = LoadText("ini.txt")
Global Line:String[] = All.Split("~r~n")

For Local i:Int=0 Until Line.length
Print "line " + i + " :" + Line[i]
Next


This works fine. All lines are accepted.



Now we do a second split action with each line to get left side and right side separated. We search for the  letter =

Code (BlitzMax) Select
...
For Local i:Int=0 Until Lines.length
Local Parts:String[] = Lines[i].Split("=")
Print "Variable " + Parts[0]  + "  has the value " + Parts[1]
Next


Our app fails at the 4th line (the line with the comment). The reason is there is no "=" in this line. So the second array element Parts[1] does not exist

A little change will help

Code (BlitzMax) Select
...
For Local i:Int=0 Until Lines.length
Local Parts:String[] = Lines[i].Split("=")
If Parts.Length>1
Print "Variable " + Parts[0]  + "  has the value " + Parts[1]
Endif
Next


AnyText.Contains("=") is another Method. It checks whether a letter (here "=") is inside a string and returns TRUE or FALSE

Now the app runs perfect. Lets see what we can do with this knowledge....


Reading a STRING

Code (BlitzMax) Select
SuperStrict
Global Ini:String[]
IniLoad("ini.txt")

Global PlayerName:String= IniRead("Name")

Print "Players Name is " + PlayerName

Function IniLoad(FileName:String)
Local All:String = LoadText("ini.txt")
Ini = All.Split("~r~n")
End Function


Function IniRead:String(Key:String)
For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")
If Parts[0]=Key Then Return Parts[1]
Next
Return Null
End Function


Do you recognize our old code? The loading of the INI-File moved into a function. The temporary variable All becomes LOCAL, because we do not need the text file as a whole outside the function.

The second function IniRead() scans all lines of the Ini-Array and returns a value if it find the key "Name".


Reading an INTEGER or DOUBLE

Code (BlitzMax) Select
Global PlayerAge:Int = IniRead("Age")

If we try to do the same for the players age, this would cause a runtime error, because PlayerAge is a INTEGER, but our function always returns STRINGs

So we have to convert the returned STRING into a INTEGER

Code (BlitzMax) Select
SuperStrict
Global Ini:String[]
IniLoad("ini.txt")

Global PlayerName:String   = IniRead("Name")
Global PlayerAge:Int       = IniRead("Age").ToInt
Global PlayerHealth:Double = IniRead("Health").ToDouble

Print "Players name is "        + PlayerName
Print "Players age is "         + PlayerAge
Print "Players health is "      + PlayerHealth
....


... = String.ToInt is another STRING METHOD and converts a STRING into an INTEGER

... = String:ToDouble is another STRING METHOD and converts into an DOUBLE floating point

Because the type that IniRead() returns is STRING we can handle the function like a string and extend the STRING METHODs: IniRead(...).ToInt



How to read an array?

for the last text line...
QuoteGameGadgets=4|123|0|4|-7
....we have to convert the right side of the line into an array. It contains 5 INTEGER values separated by a pipe letter: |. We separate them again with the Split()-Method

Code (BlitzMax) Select
....
Global PlayerGadget:Int[]  = SplitToArray( IniRead("GameGadgets") )
....
Print "Players  2nd Gadget is " + PlayerGadget[1]
....

Function SplitToArray:Int[](RightSide:String)
Local Parts:String[] = RightSide.Split("|")
Local Integer:Int[Parts.Length]
For Local I:Int=0 Until Parts.Length
Integer[i]  = Parts[i].ToInt
Next
Return Integer
End Function



As we cannot cast STRING[]-arrays directly into INTEGER[]-arrays we do it in three steps:

  • First split the content into a local STRING[]-array.

  • Then define an local INTEGER-array with the same number of element like the STRING[]-array

  • Then copy each single element from STRING[]-array to INTEGER[]-array with the ToInt()-Method

  • At the end we return the INTEGER-array to the main app.



Two code line may look tricky for you:


RETURN ARRAY

Functions can return arrays:

Code (BlitzMax) Select
Function SplitToArray:Int[] (...)
Local Integer:Int[...]
....
Return Integer
End Function

We cannot only return single variables, but also complete arrays


NEST FUNCTIONS

We can nest function calls into function calls:

Code (BlitzMax) Select
SplitToArray( IniRead("GameGadgets") )

IniRead() is a function that needs "GameGadgets" as parameter. It returns the right side of a text line of our Ini-file.

SplitToArray() is a function that needs the right side of a text line as parameter. It returns this right side splitted into an array.

So to say the result of IniRead() is the parameter for SplitToArray().

You can nest as many function calls as you like... as long as you understand your code yourself  ::)





Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/

...back from North Pole.

Midimaster

#24
Lesson XXI: Loading Levels From Files and Encapsuled Functions

Today we will learn, why a function without GLOBAL variables is more useable. This is a step ahead in your skills. And we expand the INI-file to SECTIONS to be able to load the same set of variables with different values. This is neccessary when we have different levels in our game.



GLOBAL LOCAL and Encapsuled Functions

The main target of creating functions is to use them often in your code and use them them in more than one app.

Therefore the function needs to be absolut independent from the main code. The function is not allowed to use (refer) to a GLOBAL variable. The only allowed way to communicate is the parameter list for input and the return value for output.

You can easily check with BlitzMax, whether your function  meets these requirements. Copy them to a new empty TAB in the BlitzMax IDE and the start F5. We can do this with the loading function IniLoad() from our last chapter:

Code (BlitzMax) Select
Function IniLoad(FileName:String)
        Local All:String = LoadText("ini.txt")
        Ini = All.Split("~r~n")
End Function



You will receive a runtime error because the function still refers (needs) a global array Int:String[]


Now let us modify the function toward a more universal usability:

Code (BlitzMax) Select
Function IniLoad:String[] (FileName:String)
Local All:String = LoadText("ini.txt")
Local Ini:String[] = All.Split("~r~n")
Return Ini
End Function


Now we use a LOCAL array Ini:String[] to do our splitting. At the end we return it to the main app. This also needs changes in the header of the function. Her we define what kind of variable the function will return.


Code (BlitzMax) Select
Function IniLoad:String[] (...)

...means the function returns a String-array.

You can test the function in a separate TAB by "starting" it F5 and you will see, that there is no more runtime error.



As we modified the function, we now need to edit the main code from:

Code (BlitzMax) Select
SuperStrict
Global MyIni:String[]
IniLoad("ini.txt")
...


towards...

Code (BlitzMax) Select
SuperStrict
Global MyIni:String[] = IniLoad("ini.txt")
...



And now I can demonstrate you the advantage of that new encapsuled function. Let's say we need two Ini-Files, one for the levels and one for the design. we now can use the function IniLoad() twice:


Code (BlitzMax) Select
SuperStrict
Global LevelIni:String[] = IniLoad("LevelIni.txt")
Global DesignIni:String[] = IniLoad("DesignIni.txt")
...


this was not possible before.

So now lets check the second function IniRead()

Code (BlitzMax) Select
SuperStrict

old version:

Function IniRead:String(Key:String)
For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")
If Parts[0]=Key Then Return Parts[1]
Next
Return Null
End Function


It does not survive the F5-test. Runtime error because it uses the global Ini[]-array. But in this case we need the informations of the global Ini[]-array inside the function. The solution is to send the whole array as a parameter to the function:

new version:
Code (BlitzMax) Select
Function IniRead:String(Key:String, Ini:String[])
For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")
If Parts[0]=Key Then Return Parts[1]
Next
Return Null
End Function


As we modified the function, we now need to edit the main code from:

Code (BlitzMax) Select
Global PlayerName:String   = IniRead("Name")
Global PlayerAge:Int       = IniRead("Age").ToInt
Global PlayerHealth:Double = IniRead("Health").ToDouble
Global PlayerGadget:Int[]  = SplitToArray(IniRead("GameGadgets"))
...


towards...

Code (BlitzMax) Select
Global PlayerName:String   = IniRead("Name", MyIni)
Global PlayerAge:Int       = IniRead("Age", MyIni).ToInt
Global PlayerHealth:Double = IniRead("Health", MyIni).ToDouble
Global PlayerGadget:Int[]  = SplitToArray(IniRead("GameGadgets", MyIni))
...




Ini-Files for Levels

If you have different levels in your game, this means you will to restart your game, but the values of the variables will differ from level to level. In Level 1 the number of enemies will be 10, but in Level 2 you will start with 20 and the moving speed of the enemies could be higher, etc..

This would mean we need to have several version of the settings in one INI-file:

Name=Peter
Age=25
Health=3.235
GameGadgets=4|123|0|4|-7

Name=Tom
Age=41
Health=4.111   
GameGadgets=1|2|3|4|5

Name=Linda
Age=18
Health=12.25
GameGadgets=22|11|33|77|0


This would not work with our function IniRead(), because it always immendiately returns, when it finds the first existence of a key like "Name".


Sections

Modify the Ini.Txt like this:

[Level 1]
Name=Peter
Age=25
Health=3.235
GameGadgets=4|123|0|4|-7

[Level 2]
Name=Tom
Age=41
Health=4.111   
GameGadgets=1|2|3|4|5

[Level 3]
Name=Linda
Age=18
Health=12.25
GameGadgets=22|11|33|77|0


The lines with keys in brackets are called sections. They mark the beginning of a new chapter. Lets see how we handle them in the ReadIni()


Code (BlitzMax) Select
PlayerName = IniRead("Name", MyIni, "Level 1")
...
Function IniRead:String(Key:String ,Ini:String[] , Section:String)
Section = "[" + section + "]"
Local Found:Int=False

For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")

If Parts[0]=Section Then Found=True

If Found=True Then
If Parts[0]=Key Then Return Parts[1]
EndIf
Next
Return Null
End Function


The function now needs three parameters. The new parameter is the name of the section. Inside the function we join the brackets to the 3rd parameter. Level 1 becomes [Level 1]

Then we scan the list of lines until we find this expression. The scan for the key word Name is prevented because the variable Found is not TRUE at the moment

From this moment on we allow also the scan for the key word Name. It will be found within the next iterations.


The complete code


Code (BlitzMax) Select
SuperStrict
Graphics 800,600
Global MyIni:String[] =IniLoad("ini.txt")
Global Level:Int=1

Global PlayerName:String, PlayerAge:Int, PlayerHealth:Double, PlayerGadget:Int[]
ChangeLevel Level
Repeat

Cls
If KeyHit(KEY_L)
Level = Level +1
ChangeLevel Level
EndIf

DrawText "current level is " + Level, 100,100

DrawText "Players name is "        + PlayerName      , 100, 200
DrawText "Players age is "         + PlayerAge       , 100, 230
DrawText "Players health is "      + PlayerHealth    , 100, 260
DrawText "Players  2nd Gadget is " + PlayerGadget[1] , 100, 290

DrawText "press L to change level", 100,400
Flip

Until AppTerminate()


Function ChangeLevel(Level:Int)
If Level=4 End
PlayerName    = IniRead("Name", MyIni, "Level " +Level)
PlayerAge     = IniRead("Age", MyIni, "Level " +Level)ToInt
PlayerHealth  = IniRead("Health", MyIni, "Level " +Level).ToDouble
PlayerGadget  = SplitToArray(IniRead("GameGadgets", MyIni, "Level " +Level))
End Function


Function IniRead:String(Key:String , Ini:String[] , Section:String)
Section = "[" + section + "]"
Local Found:Int=False
For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")

If Parts[0]=Section Then Found=True

If Found=True Then
If Parts[0]=Key Then Return Parts[1]
EndIf
Next
Return Null
End Function


Function IniLoad:String[](FileName:String)
Local All:String = LoadText("ini.txt")
Local Ini:String[] = All.Split("~r~n")
Return Ini
End Function


Function SplitToArray:Int[](RightSide:String)
Local Parts:String[] = RightSide.Split("|")
Local Integer:Int[Parts.Length]
For Local I:Int=0 Until Parts.Length
Integer[i]  = Parts[i].ToInt
Next
Return Integer
End Function





Challenge I: Encapsuled Functions

Scan all lesson from VI to XX and check all function you can see. Find those functions, which meet the new conditions and are already universal.



Challenge II: Optimize The Functions

Scan all lesson from VI to XX and check all function you can see. Find those functions, which DO NOT meet the new conditions. Try to convert them into a universal style


Challenge III: Quiz Game

Write a quiz game, where the user has to answer 10 questions by selceting one of three multiple choice answers. Add a Ini-file that contains the 10 questions and 3 possible answers and also a flag, which answer is the right.





Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/
...back from North Pole.

Midimaster

#25
Lesson XXII: Saving Variables And Complete Levels To INI-Files


Today you will learn how to save someting to your INI-File. You can use this for saving variables or the current state of a game to continue it tomorrow. Or we can use it to save a complete maze after you created it in your editor or every time when the random generator produces a nice looking maze.

Advise:
Target of my lessons are never to code together with you a "perfect INI-system", but teach you strategies to expand app in little steps, find bugs and react on them. So we develop here a very restriced INI-system, which not enables to add new keys or new sections from within the app. Means: we overwrite only already existing keys.



IniSave()


Is a easy function with another nice STRING METHOD. We have to join the lines of our array back to a single text file, than save it in one go. It looks a little bit like the IniLoad():

Code (BlitzMax) Select
Function IniSave(FileName:String,Ini:String[])
Local All:String = "~r~n".Join(ini)
SaveText All, FileName
End Function



Text:String = SEPARATOR.Join(Array[]) is the reverse function to the method Split(). It connects all member of a string array and inserts the Separator ASCII between them.

As we want to have indepenent lines also in our text-file we use again the separator "~r~n" (CRLF).



IniWrite()

We start with the complete code from lesson XXI and add a IniWrite() function

The IniWrite() will have a lot of the code line our IniRead() already has, because also IniWrite() needs to find the correct line. So we could copy the function IniRead() and modify the copy until it writes values:

Code (BlitzMax) Select
Function IniWrite(NewValue:String, Key:String , Ini:String[] , Section:String)
Section = "[" + section + "]"
Local Found:Int=False

For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")

If Parts[0]=Section Then Found=True

If Found=True
If Parts[0]=Key
Part[1] = NewValue     ' <------- THE ONLY CHANGE
Return
EndIf
EndIf
Next
Return Null
End Function


You see it is nearly the same. We have a new parameter NewValue:String which contains, what we want to write into the INI-line. And the codeline "after we detected the key" now does not return a value but changes Part[1].


No double code

But a better idea would be to encapsul the feature "finding" into a new function and not have double code passages. Therefore we write a third function SearchLine() which returns the line number, where we found the key.

Code (BlitzMax) Select
Function SearchLine:Int(Key:String ,Ini:String[] , Section:String)
Section = "[" + section + "]"
Local Found:Int=False

For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")

If Parts[0]=Section Then Found=True

If Found=True
If Parts[0]=Key
Return i ' <------- THE ONLY CHANGE
EndIf
EndIf
Next
Return Null
End Function


It is again nearly the same like IniRead(), but it does not return the content of the line, but the line number.

Now the IniRead() can be changed an becomes a much shorter:

Code (BlitzMax) Select
Function IniRead:String(Key:String , Ini:String[] , Section:String)
Local LineNumber:Int = SearchLine(Key, Ini, Section)
Local Parts:String[] = Ini[LineNumber].Split("=")
If Parts[0]=Key Then Return Parts[1]
Return Null
End Function



And also the IniWrite is now massive shorter:

Code (BlitzMax) Select
Function IniWrite(NewValue:String, Key:String , Ini:String[] , Section:String)
Local LineNumber:Int = SearchLine(Key, Ini, Section)
Local Parts:String[] = Ini[LineNumber].Split("=")
If Parts[0]=Key
Parts[1] = NewValue
EndIf
End Function




You may think that it is useless to extract double code passages into a new function. But this solves one of the main problems beginners have: They cannot understand their own code after it grows to a monster. Think about the moment when we find out, that our "finding algorithm" is not perfect or has mistakes... In our old code you wouldd need to make changes at several places. With the new code we change it in one functions and all the calling code lines will profit immediately!!! And I can say already now: Our "finding algo" has massiv problems, but we cannot see them at the moment!



Really changing the INI?

Perphaps you already checked that we made a mistake?

Here is our playground: Try the new version of our app:

Code (BlitzMax) Select
SuperStrict
Graphics 800,600
Global MyIni:String[] =IniLoad("ini.txt")
Global Level:Int=1

Global PlayerName:String, PlayerAge:Int, PlayerHealth:Double, PlayerGadget:Int[]
ChangeLevel Level
Repeat

Cls
If KeyHit(KEY_L)
Level = Level +1
ChangeLevel Level
EndIf

If KeyHit(KEY_C)
IniWrite "HORST", "Name" , MyIni, "Level " + Level
EndIf


DrawText "current level is " + Level, 100,100

DrawText "Players name is "        + PlayerName      , 100, 200
DrawText "Players age is "         + PlayerAge       , 100, 230
DrawText "Players health is "      + PlayerHealth    , 100, 260
DrawText "Players  2nd Gadget is " + PlayerGadget[1] , 100, 290

DrawText "press L to change level", 100,400
DrawText "press C to change name to HORST", 100,430
Flip

Until AppTerminate()


Function ChangeLevel(Level:Int)
If Level=4 Then Level=1
PlayerName    = IniRead("Name", MyIni, "Level " +Level)
PlayerAge     = IniRead("Age", MyIni, "Level " +Level).ToInt
PlayerHealth  = IniRead("Health", MyIni, "Level " +Level).ToDouble
PlayerGadget  = SplitToArray(IniRead("GameGadgets", MyIni, "Level " +Level))
End Function


Function SearchLine:Int(Key:String ,Ini:String[] , Section:String)
Section = "[" + section + "]"
Local Found:Int=False

For Local i:Int=0 Until Ini.Length
Local Parts:String[] = Ini[i].Split("=")

If Parts[0]=Section Then Found=True

If Found=True
If Parts[0]=Key
Return i ' <------- THE ONLY CHANGE
EndIf
EndIf
Next
Return Null
End Function


Function IniRead:String(Key:String , Ini:String[] , Section:String)
Local LineNumber:Int = SearchLine(Key, Ini, Section)
Local Parts:String[] = Ini[LineNumber].Split("=")
If Parts[0]=Key Then Return Parts[1]
Return Null
End Function


Function IniWrite(NewValue:String, Key:String , Ini:String[] , Section:String)
Local LineNumber:Int = SearchLine(Key, Ini, Section)
Local Parts:String[] = Ini[LineNumber].Split("=")
If Parts[0]=Key
Parts[1] = NewValue
EndIf
End Function


Function IniLoad:String[](FileName:String)
Local All:String = LoadText("ini.txt")
Local Ini:String[] = All.Split("~r~n")
Return Ini
End Function


Function SplitToArray:Int[](RightSide:String)
Local Parts:String[] = RightSide.Split("|")
Local Integer:Int[Parts.Length]
For Local I:Int=0 Until Parts.Length
Integer[i]  = Parts[i].ToInt
Next
Return Integer
End Function


In this version you can test the IniWrite() with pressing key <C> (Change). This will change the current players name to "HORST". But if you playaround you will notice that nothing happens in the display.

the current IniWrite() changes the variable Part[1]. But this is only a local array inside the function, this would not change the INI permanent. To change the INI we need to manipulate the array Ini[] with the original lines!!! So we need this in the IniWrite()

Code (BlitzMax) Select
Function IniWrite(NewValue:String, Key:String , Ini:String[] , Section:String)
Local LineNumber:Int = SearchLine(Key, Ini, Section)
Local Parts:String[] = Ini[LineNumber].Split("=")
Print "IniWrite at " + linenumber + " ->" + Ini[LineNumber]
If Parts[0]=Key
Ini[LineNumber] = Parts[0] + "=" + NewValue   ' <------- THE ONLY CHANGE
Print "changed to ->" + Ini[LineNumber]
EndIf
End Function


At lot of PRINT commands are the best friend of a developer. With PRINTs you can see what really happens. Add this PRINT commands to the IniWrite(). The first PRINT...
Code (BlitzMax) Select
Print "IniWrite at " + linenumber + " ->" + Ini[LineNumber]
... verifies that we truely entered the function. The second...
Code (BlitzMax) Select
Print "changed to ->" + Parts[1]
... verifies that we really change the Ini[LineNumber]

When you run the app you can see, that the line content changed. But the displayed name not! What happened? But it becomes even more obscure: Press key <L> 3 times!



Challenge I: Find out what happened and try to fix it

Find out what happened and try to fix it. Add as many PRINT as you like.



Challenge II: A Ini for The Maze

Try to add an ini system for our maze game. A random generated maze can be stored to a INI. When it is good looking press "S" and store it for eternity. Press a number between 1 and 9 to re-call previous stored mazes.



Challenge III: Expand the INI-System

Expand the INI-system towards adding new sections and keys. This means increasing the size of an array... difficult!
Or much easier: Create a bigger empty array and copy the contents of Ini[] array into it, while adding lines for the new keys and sections... At the end rebuild the Ini[] array from this.





Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/
...back from North Pole.

Midimaster

#26
Lesson XXIII: Three little step towards OOP... Step 1

I was asked to write some lessons about Object Oriented Programming. There are a lot of very good tutorials, even Brucey wrote a very good one: 

https://blitzmax.org/docs/en/tutorials/oop_tutorial

The problem is... they move too fast. As it is a big step for beginner, beginners need to see a reason for OOP and need to feel the advantage of using it. So we will proceed very very slow and with a lot of typical usages: The lessons XXIII to XXV will be  "3 little steps" and lesson XXVI will code a home made GUI.


First Little Step: A User Defined Type

In our games the actors often need two coordinates X and Y. We need them to descripe the position of players and enemies, etc...
Global PlayerX:Int = 20
Global PlayerY:Int = 100


And for 100 Enemies we could realize this in two arrays:

Global EnemyX:Double[100] = Rand(0,800)
Global EnemyY:Double[100] = Rand(0,600)


And with a personal moving speed we are at 4 arrays, if we add "health" we get a 5th array,  and so on...

Global EnemyX:Double[100] = Rand(0,800)
Global EnemyAddX:Double[100] = Rnd(0,1)

Global EnemyY::Double[100] = Rnd(0,600)
Global EnemyAddY:Double[100] = Rnd(0,1)

Global EnemyHealth:Int[100] = 100


Not nice.. but acceptable. But what, if the enemies should also get individual names and images and magazine and.... Still acceptable?

Think about a game situation, where we need to compare two enemies, what a staple of code lines...

New rule:

Always when your app needs a couple of individual objects of the same type you should use User Defined Types.

Those objects can be: Enemies, Balls, Snowflakes, chess pieces, cards, jigsaws pieces. But also buttons, sliders, data records, notes, tables...

A User Defines Type is a author-made variable type, that can contain any number of variables (often called properties or fields)


At first
...we need to descripe our home-made new Type:

Code (BlitzMax) Select
SuperStrict
Type TActor
     Field X:Double, Y:Double, addX:Double, addY:Double, Name:String
End Type


Type TName ... End Type defines begin and end of the description block. You can select a free name for your new type. But it is common to start the name with a T: TName, TActor, TImage, TSound... (Yes you already heard this. Also TImage is such a User Defines Type defined by a prior user. Later it became part of BlitzMax)

Field variable, variable, etc... defines the inside variables of your type.

After this definition TActor is a well known variable type like Int or String.


In a second step
...we create a first game figure by using the new variable type:

Code (BlitzMax) Select
...
Global Player:TActor = New TActor


This is the parallel use to...
...
Global Value:Int = 123


Because User Defines Types have more than one field, we cannot immediately set a value like =123. So we first write =New TActor (more later). This is the birth of the first member of Actors.


As last step
...we set it's 5 values:

Code (BlitzMax) Select
SuperStrict
Type TActor
     Field X:Double, Y:Double, addX:Double, addY:Double, Name:String
End Type
Global Player:TActor = New TActor
Player.X = 20
Player.addX = 0.5
...
Player.Name = "Killer"




We can use the TActor-Type also for our enemies. Also they move and live like players:

Code (BlitzMax) Select
SuperStrict
Type TActor
     Field X:Double, Y:Double, addX:Double, addY:Double, Name:String
End Type
....
Global Enemy:TActor[100]
For local i:Int=0 until 100
     Enemy[i] = New TActor
     Enemy[i].X = Rand(0,800)
     Enemy[i].addX = Rnd(0,1)
     ...
     Player.Name = "Enemy no " + i
Next


It is important to start each new member with the =New TActor command. With Global Enemy:TActor[100] the 100 enemies are not born yet. The birth is the individual New-command for each member!!!

From now on you can use the coordinates like two properties of the actor. Use the dot to join member and field:


Code (BlitzMax) Select
SuperStrict
Type TActor
     Field X:Double, Y:Double, addX:Double, addY:Double, Name:String
End Type
....
Player.X = Player.X + Player.addX
If Player.X< 0 then
      ' left screen border
     Player.X=0
Endif
...
DrawText Player.Name, 100, 100

For local i:Int=....
     If (Enemy[i].X > Player.X) And (Enemy[i].Y> Player.Y)...
          ' kill an enemy
           Enemy[i] =NULL
     Endif
....




Challenge I: Ball.X, Ball.Y, Ball.addX, Ball.addY

Try to re-write the ping pong game. Exchange all ball related variables to OOP. Instead of BallX use now Ball.X, etc...


Challenge II: Contact & Adress Book

Try to write an app, which manage adresses and contact details of your friends. Invent a TContact type and think about, which FIELDS are necessary. If you want you can combine it with your INI-skills


Challenge III: BreakOut Arkanoid


Start to write a Breakout/Arkanoid-Game. 50 stones in 5 rows need to be destroyed by a ball. Use your type TBrick 






Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/
...back from North Pole.

Midimaster

#27
Lesson XXIV: A Type Is A Class? (OOP Step 2)



If you ever want to use your code in more than one app or if you plan to give the code away to others  as a library, you need to encapsul the code, that it never touches the main code.

Touches?

You could accidentally use variable names in your library, that also are used in the main code. This would cause unpredictable effect on the main code. The same happens with function names.

We already learned that function should only use LOCAL variables. But often a library need to have common variables over all their functions. Here a CLASS would help. A Class is a library with a unique namespace, means all variable names and function names are guaranteed unique. But how to quarantee this?

Put It Into A Type

We can put GLOBAL variables and Functions into a TYPE definition. And then they are ancapsuled from the rest of our app.

Here we have a main code and a class that both use the same variable name and the same function name, but BlitzMax can handle it. If you run the app (F5) the IDE does not report syntax-Errors. This means the code is valid:

Code (BlitzMax) Select
SuperStrict
Global Name:String = "Peter"
ReName()
Print Name

Function ReName()
Name="Rosi"
End Function

Type MyClass
Global Name:String="Tom "

Function ReName()
Name = Name + "Dooley"
End Function
End Type



How to use this libraries?

If you want to call a library function, write a compound word ClassName + Dot + FunctionName

If the inside function uses a GLOBAL variable, it preferes the inside GLOBAL if it exists.

Code (BlitzMax) Select
SuperStrict
Global Name:String = "Peter"
MyClass.Rename()   '   <--------- CALL a library function
Print Name
Print MyClass.Name '   <--------- CALL a library variable
Function ReName()
Name="Rosi"
End Function


Type MyClass
Global Name:String="Tom "

Function ReName()
Name = Name + "Dooley"
End Function
End Type


To check what happend we ask for the inside global variable Name: MyClass.Name

If you want to work with a library variable write a compound word ClassName + Dot + VariableName


Change the Class Name in your main app

If you do not like the given library name you can also exchange it. We call this trick: Create an instance of the Type:
Code (BlitzMax) Select
SuperStrict
Global HisClass:NameallocationInfinitvWorkGroup = New NameallocationInfinitvWorkGroup

HisClass.Rename()
Print HisClass.Name

' here a class with a terrible name:
Type NameallocationInfinitvWorkGroup
Global Name:String="Tom "

Function ReName()
Name = Name + "Dooley"
End Function
End Type



A Universal Stop Watch


This stop watch can be used in any main code. It is completely indepenent from the main development. At the moment it knows only one job: reporting the seconds

Code (BlitzMax) Select
SuperStrict
Graphics 200,200
Repeat
Cls
DrawText TStopWatch.Tick(),100,100
Flip 0
Until AppTerminate()


Type TStopWatch
Global Ticks:Int, NextTime:Int=Millisecs(), TimeIntervall:Int=1000

Function Tick:Int()
If NextTime<MilliSecs()
NextTime = MilliSecs()+TimeIntervall
Ticks=Ticks+1
EndIf
Return ticks
End Function
End Type



But we can adjust it to shorter intervals:


Code (BlitzMax) Select
SuperStrict
Graphics 200,200
Repeat
Cls
DrawText TStopWatch.Tick(),100,100
DrawText "Press R to RESET", 30,150
If KeyHit(KEY_R)
TStopWatch.Reset 10  ' now faster every 10msec
EndIf
Flip 0
Until AppTerminate()


Type TStopWatch
Global Ticks:Int, NextTime:Int=MilliSecs(), TimeIntervall:Int=1000

Function Reset(Intervall:Int)
TimeIntervall = Intervall
Ticks=0
NextTime=MilliSecs()
End Function

Function Tick:Int()
If NextTime<MilliSecs()
NextTime = NextTime+TimeIntervall
Ticks=Ticks+1
EndIf
Return ticks
End Function
End Type


Another  inside function Reset() manipulates the inside variables


Do whatever you want: A Clock Gadget



Now you can code whatever you want. The TTimer is a closed world like an app in the app:


Code (BlitzMax) Select
SuperStrict
Graphics 300,250
TStopWatch.Reset 100
SetClsColor 0,33,66
Repeat
Cls
SetColor 0,66,99
DrawOval 50,20,200,200
Local Degree:Double, x:Double, y:Double
TStopWatch.Tick()

SetColor 255,0,0
Degree=TStopWatch.Degree()
x=Cos(Degree)*100 + 150
y=Sin(Degree)*100 + 120
SetLineWidth 2
DrawLine 150,120,x,y

SetColor 0,255,0
Degree=TStopWatch.SecondDegree()
x=Cos(Degree)*80 + 150
y=Sin(Degree)*80 + 120
SetLineWidth 2
DrawLine 150,120,x,y

SetColor 255,255,255
Degree=TStopWatch.MinuteDegree()
x=Cos(Degree)*60 + 150
y=Sin(Degree)*60 + 120
SetLineWidth 3
DrawLine 150,120,x,y
DrawText TStopWatch.TimeString(), 130,230
Flip 0
Until AppTerminate()


Type TStopWatch
Global Ticks:Int, NextTime:Int=MilliSecs(), TimeIntervall:Int=1000

Function Reset(Intervall:Int)
TimeIntervall = Intervall
Ticks=0
NextTime=MilliSecs()
End Function

Function Tick:Int()
If NextTime<MilliSecs()
NextTime = NextTime+TimeIntervall
Ticks=Ticks+1
EndIf
Return ticks
End Function

Function Degree:Int()
Return (ticks Mod 10) *36 -90
End Function 

Function SecondDegree:Int()
Return (Int(ticks/10) Mod 60) *6-90
End Function 

Function MinuteDegree:Int()
Return (Int(ticks/600) Mod 60) *6-90
End Function 

Function TimeString:String()
Local m:String = Int(ticks/600) Mod 60
Local s:String = Int(ticks/10) Mod 60
Local h:String = ticks Mod 10

Return m +":" + s + ":" + h
End Function
End Type
...back from North Pole.

Midimaster

#28
Button I: Simple Buttons in Games for Total Beginners

This Tutorial show how to handle some self made buttons in your game. In the first example you will see a very easy to understand solution. The we will expand this to a TYPE-solution which can handle several buttons with one code.

Advice:
User Derron points me to the problematic, that a MouseHit() call should not be inside a function, but at a central place. That is necessary, because other part of the app may also need to know the MouseState. So I define a GLOBAL variable MOUSESTATE, which keeps the state until the next FLIP

One Button

A button is a rectangle, that interact with the mouse. The image can be a PNG-file or a code painted area.
The Checkbutton() function excludes all cases, where nothing happened and returns 1 if the Mouse was inside the rectangle:

SuperStrict
Graphics 800,300
Global MouseState:Int

Repeat
    Cls
    MouseState = MouseHit(1)
    DrawRect 100,200,120,80
     If CheckButton()>0 Then
        Print "Button pressed"
    EndIf
    Flip
Until AppTerminate()

Function CheckButton:Int()
    If MouseState=0 Return 0
    Local mX:Int = MouseX()-100
    Local mY:Int = MouseY()-200
    If mX<0 Or mX>120 Return 0
    If mY<0 Or mY>80  Return 0
    Return 1
End Function

Before checking we substract the start coordinate (100/200) of the rectangle to simplify the code.


Five Buttons In A Row

Nearly the same code. We only have to divide the relative MouseX by the width of the Rectangle. This returns float values between 0.00 and 5.00. When we take the INTEGER of them we get the values 0,1,2,3,4. To receive 1,2,3,4,5 we add +1.

SuperStrict
Graphics 800,300
Global MouseState:Int

Repeat
    Cls
    MouseState = MouseHit(1)
    For Local I:Int=0 Until 5
        DrawRect 100+i*120+1, 200,118,80
    Next
    Local Pressed:Int = CheckButton()
    If Pressed>0 Then
        Print "Button " + Pressed + " pressed"
    EndIf
    Flip
Until AppTerminate()

Function CheckButton:Int()
    If MouseState=0 Return 0
    Local mX:Int = MouseX() - 100
    Local mY:Int = MouseY() - 200
    If mX<0 Or mX>5*120 Return 0
    If mY<0 Or mY>80 Return 0
    Return Int(mX/120) + 1
End Function

The same would be possible with vertical buttons. Here we calculate from MouseY and the height(80).


A Simple Button Type

To be more flexible and to handle a dozend individual buttons we define a button TYPE. This enables us to give away the control of painting, checking, etc to an automated process. Via a personal ID each button will report back, if it was pressed:

SuperStrict
Graphics 800,300
Global MyButton:TButton= New TButton(1,100,200,120,80)
Global MouseState:Int

Repeat
    Cls
    MouseState = MouseHit(1)
    MyButton.Draw
    Local Pressed:Int = MyButton.Check()
    If Pressed>0 Then
        Print "Button " + Pressed + " pressed"
    EndIf
    Flip
Until AppTerminate()

Type TButton
    Field ID:Int, X:Int, Y:Int, W:Int, H:Int
   
    Method New (id:Int, x:Int, y:Int, w:Int, h:Int)
        Self.ID = id
        Self.X  = x
        Self.Y  = y
        Self.W  = w
        Self.H  = h
    End Method 
   
    Method Draw()
        DrawRect X, Y, W, H
    End Method
   
    Method Check:Int()
        If MouseState=0 Return 0
        Local mX:Int = MouseX() - X
        Local mY:Int = MouseY() - Y
        If mX<0 Or mX>W Return 0
        If mY<0 Or mY>H Return 0
        Return ID
    End Method
End Type

A Bundle of Type Buttons

With adding a Global TList we are able to self contain all buttons inside the TYPE. Now the function DrawAll() care about the painting of all buttons and the function CheckAll:Int() checks the mouse for all buttons:

SuperStrict
Graphics 800,300
New TButton(1,100,200,120,80)
New TButton(2,234, 45,222,33)
New TButton(3,600, 100,50,180)
Global MouseState:Int

Repeat
    Cls
    MouseState = MouseHit(1)
    TButton.DrawAll
    Local Pressed:Int = TButton.CheckAll()
    If Pressed>0 Then
        Print "Button " + Pressed + " pressed"
    EndIf
    Flip
Until AppTerminate()


Type TButton
    Global All:TList = New TList
   
    Field ID:Int, X:Int, Y:Int, W:Int, H:Int
   
    Method New (id:Int, x:Int, y:Int, w:Int, h:Int)
        Self.ID = id
        Self.X  = x
        Self.Y  = y
        Self.W  = w
        Self.H  = h
        All.Addlast Self
    End Method 
   
    Function DrawAll()
        For Local loc:TButton = EachIn All
            loc.Draw
        Next
    End Function
   
    Method Draw()
        DrawRect X, Y, W, H
        DrawText id, x,y-30
    End Method
   
   
    Function CheckAll:Int ()
        If MouseState=0 Return 0
        For Local loc:TButton = EachIn All
            Local result:Int = loc.Check()
            If result>0 Return result
        Next
        Return 0   
    End Function
   
    Method Check:Int()
        Local mX:Int = MouseX() - X
        Local mY:Int = MouseY() - Y
        If mX<0 Or mX>W Return 0
        If mY<0 Or mY>H Return 0
        Return ID
    End Method
End Type




...back from North Pole.

Midimaster

#29
Button II: More Design and Functionality

After we switched now to TYPE buttons it is easy to add more features to the buttons. Because we have to code it only once and all buttons will react immediately.


Example: Selected Buttons

To enable the buttons to SELECTED/UNSELECTED we only have to add a new Property SELECTED and write reactions in DRAW() and CHECK(). Additionally we now return back not longer the pressed state as an INTEGER, but the whole button itself (WHICH). So we can now handle this "last action" button also outside the TYPE.


Advice:
User Derron points me to the problematic, that a MouseHit() call should not be inside a function, but at a central place. That is necessary, because other part of the app may also need to know the MouseState. So I define a GLOBAL variable MOUSESTATE, which keeps the state until the next FLIP


SuperStrict
Graphics 800,300
New TButton(1,100,200,120,80)
New TButton(2,234, 45,222,33)
New TButton(3,600, 100,50,180)
SetClsColor 0,155,155

Global MouseState:Int
Repeat
    Cls
    MouseState = MouseHit(1)
    TButton.DrawAll
    Local Which:TButton = TButton.CheckAll()
    If Which
        If Which.Selected=1
            Print "Button " + Which.ID + " selected"       
        Else
            Print "Button " + Which.ID + " un-selected"
        EndIf
    EndIf
    Flip
Until AppTerminate()


Type TButton
    Global All:TList = New TList
   
    Field ID:Int, X:Int, Y:Int, W:Int, H:Int
    Field Selected:Int
   
    Method New (id:Int, x:Int, y:Int, w:Int, h:Int)
        Self.ID = id
        Self.X  = x
        Self.Y  = y
        Self.W  = w
        Self.H  = h
        All.Addlast Self
    End Method 
   
    Function DrawAll()
        For Local loc:TButton = EachIn All
            loc.Draw
        Next
    End Function
   
    Method Draw()
        SetColor 1,1,1
        DrawRect X, Y, W, H
        SetColor 111,111,111
        DrawRect X+1, Y+1, W-2, H-2
        SetColor 155,155,155
        If Selected=1
            SetColor 155,255,155       
        EndIf
        DrawRect X+4, Y+4, W-7, H-7
        SetColor 1,1,1
        DrawText id, x + (w-TextWidth(id))/2,y + (h-TextHeight("T"))/2+2
    End Method
   
   
    Function CheckAll:TButton ()
        If MouseState=0 Return Null
        For Local loc:TButton = EachIn All
            Local result:Int = loc.Check()
            If result>0 Return loc
        Next
        Return Null   
    End Function
   
    Method Check:Int()
        Local mX:Int = MouseX() - X
        Local mY:Int = MouseY() - Y
        If mX<0 Or mX>W Return 0
        If mY<0 Or mY>H Return 0
        Selected=1-Selected
       
        Return ID
    End Method
End Type



Example Radio Group Buttons

If you need to de-select the other buttons when pressing a new button we talk about RADIO-BUTTONS. Here is a solution, that cares about this.

RadioButton.gif
(the GIF animation does not show the real shading quality. Check the app!)

Every time, when a Check() is successful, it calls the method DeSelect() to reset the other buttons. Therefore, we need a variable RADIO-GROUP to know which buttons belong to the same group.

    Method Check:Int()
        Local mX:Int = MouseX() - X
        Local mY:Int = MouseY() - Y
        If mX<0 Or mX>W Return 0
        If mY<0 Or mY>H Return 0
        Selected=1-Selected
        If Selected=1
Deselect RadioGroup
EndIf
        Return ID
    End Method

Method DeSelect(Group:Int)
If Group=0 Return
For Local loc:TButton=EachIn All
If loc=Self Continue
If loc.RadioGroup=Group
loc.Selected=0
EndIf
Next
End Method

We scan all buttons and select each, that belong to the group. To prevent switching off the newly pressed button we have to exclude SELF.


Here is the complete code:
SuperStrict
Graphics 800,400
For Local i:Int= 0 To 5
New TButton(i+1,100+i*66,200,66,122, 1)
Next

SetClsColor 0,155,155

Global MouseState:Int
Repeat
    Cls
    MouseState = MouseHit(1)
    TButton.DrawAll
    Local Which:TButton = TButton.CheckAll()
    If Which
        If Which.Selected=1
            Print "Button " + Which.ID + " selected"       
        Else
            Print "Button " + Which.ID + " un-selected"
        EndIf
    EndIf
    Flip
Until AppTerminate()


Type TButton
    Global All:TList = New TList
   
    Field ID:Int, X:Int, Y:Int, W:Int, H:Int
    Field Selected:Int, RadioGroup:Int
Global Images:TImage
   
    Method New (id:Int, x:Int, y:Int, w:Int, h:Int, group:Int)
If Images=Null
Images=LoadAnimImage("VintageButton.png",66,122,0,4)
EndIf
        Self.ID = id
        Self.X  = x
        Self.Y  = y
        Self.W  = w
        Self.H  = h
Self.RadioGroup = group
        All.Addlast Self
    End Method
   
    Function DrawAll()
        For Local loc:TButton = EachIn All
            loc.Draw
        Next
SetColor 255,255,255
DrawImage  Images,0,0,0
    End Function
   
    Method Draw()
        SetColor 255,255,255
Local ImageNr:Int = FindBest()
        DrawImage Images,X,Y,ImageNr
     End Method
   
Method FindBest:Int()
If Selected=0 Return 0
Return 3
End Method

    Function CheckAll:TButton ()
        If MouseState=0 Return Null
        For Local loc:TButton = EachIn All
            Local result:Int = loc.Check()
            If result>0 Return loc
        Next
        Return Null   
    End Function
   
    Method Check:Int()
        Local mX:Int = MouseX() - X
        Local mY:Int = MouseY() - Y
        If mX<0 Or mX>W Return 0
        If mY<0 Or mY>H Return 0
        Selected=1-Selected
        If Selected=1
Deselect RadioGroup
EndIf
        Return ID
    End Method

Method DeSelect(Group:Int)
If RadioGroup=0 Return
For Local loc:TButton=EachIn All
If loc=Self Continue
If loc.RadioGroup=Group
loc.Selected=0
EndIf
Next
End Method
End Type



You need this image to run the example:
VintageButton.png
 


Advise:
Critics to this tutorial are welcome, but please do not post here, but there:

https://www.syntaxbomb.com/blitzmax-blitzmax-ng/critics-and-advises-to-blitzmax-tutorial/
...back from North Pole.