What do i have to be able to do?
Nothing...
This will be a tutorial in a lot of very small lessions. Each step can be learned in 10 Minutes. If you don't know anything about writing games or apps you will be right here! It would help if you know a little bit about Mathematics and Computers.
What do I need to install?
Nothing!
We will use BlitzMax NG, because the installation is super easy: You need to download and (unzip) nothing more than one single file.
Download:
https://blitzmax.org/downloads/
Installation Manual:
https://blitzmax.org/docs/en/setup/get_started/
Where can I get help?
Here in the Forum!
Please do not post into this tutorial. Feel free to start as many additional threads here:
https://www.syntaxbomb.com/blitzmax-blitzmax-ng/
Now lesson 1
Open BlitzMax, write the following lines and then press F5 to start your first program:
Advise:
User TomToad tells us, that the very first start (direct after installation of BlitzMax) can last 2 minutes. The future starts with F5 will only need some seconds until your app appears. Background: BlitzMax needs to prepare hundred of libraries and the reference manual in the very first moment.)
Graphics 600,400
DrawText "Hello World", 100,200
Flip
WaitKey
Graphics opens a window with a size of 600x400 Pixel
DrawText writes a text at the position 100 pixel from left, 200 pixel from top into the window. You can read BASIC instruction like normal "english sentences": "Please draw the text 'Hello World' at the coordinate (100,200) "
Flip is a must-have and makes the content of the window visible (more later)
WaitKey waits for a keypress of the user
After WaitKey there is no further codeline. So, when the user press any key your app stops.
Challenge I: Write in every corner
Try to write a app, that writes a word in every corner of the screen.
Challenge II: Small App
Try to create a window, that has only 200x200pix.
Challenge III: Test the limits
Now play around with this commands, change parameters or text content and observe, what happens. You can add as many DrawTexts as you like. But Graphics and Flip are only allowed one time an app!!
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/
Lesson II: A Minimum Template For All Your AppsOur next example expands the app to the typical 3-part structure:
- pre-code-zone use this zone for: defining variables, open connections, load images , etc...
main loop-zone use this zone for: moving objects, playing sounds, etc..
finish code-zone use this zone for: save files at the end, close connections, etc...
'pre-code-zone:
Graphics 600,400
Global x=5
'main-loop-zone:
Repeat
Cls
DrawRect X,200,5,5
x = x+1
Flip
Until AppTerminate()
' finish-code-zone:
Print x
Global defines a variable. In BlitzMax NG you can use new Variablename only, when you defined it. This prevents typos.
Advise:
User Hotshot tells us, that the BlitzMax NG forces us to define new variablename before using it. This can be done with a Local or a Global command (more later...)Repeat ... Until defines beginning and end of a code loop. The code between this two markers will execute again and again.
Until AppTerminate() enables to leave the main loop, when the user clicked on the "Top-Right-X-In-Window"
Cls (clear screen) will clean the window for the next round of drawing.
Print sends a log text to the BlitzMax Output-TAB. This enables the developer to observe the program-flow and the variable's contents.
DrawRect is another of the
Draw...commands and draws a rectangle. Other commands are:
DrawLine
DrawRect
DrawOval
DrawText
and two more complex:
If you want to know more about these commands and how they are used, write the command in a code line and tab twice on F1-key. The BlitzMax HELP will open the reference of this command
The Program FlowPrograms run the code top down. But commands like
Repeat...Until enable to jump again to a top marker. When the program reaches the
Until it checks a "condition" and then normaly jumps up to the line with
Repeat. But if the condition is true then it leaves the loop and works the next code line below the
Until. In our example the "condition" is
AppTerminate(), a function that checks whether the user closed the window. So the loop keeps looping until the user closes the window. You can read BASIC instruction like normal "english sentences": "
Please repeat until the user closes the window!"
Within the main loop we paint the whole screen again and again. It start with a black screen
Cls followed by all the elements to want to show. This can be hundreds ore thousands... it does not matter, BlitzMax is fast enough to do it in lightspeed. To prevent that the user might watch and notice this sequential painting, all the painting happens in the background until
Flip brings it to the foreground.
VariablesThis...
... X=5
... looks like you know it from Mathematics (spoken: "X equals 5"), but there is a difference... In BASIC we should speak "Let X now be 5". So this is not an equation, but more like a command.
When thinking this way you can understand this line:
X=X+1
Mathemathical nonsense, but as a command it says: "from now on let x be the result of (x+1)". This means if x is still 5, after this command it will be 6. And in our example (because of the Repeat-Until-Loop) it will become 7 and 8 and so on...
You can use single letters as variable name, but also long words, except these words that are already commands like
DrawRect In our case this
X is counted up in the main loop. And as a part of the DrawRect command line it moves the rectangle from left to right side.
And these lines like ' pre-code-zone ?all line with an apostrophe ' at the beginning are no commands but comments. You can write whatever you want, it will not become a part of the app. We use this only for developers notice.
Challenge I: Move the Rectangle other waysTry to change the code, that the rectangle move from top to bottom
Challenge II: Resize A CircleTry to change the code, that a Circle (Oval) increases in size, while moving from right bottom to left top
Challenge III: Play with Draw... commandsNow try to add other Draw...commands and learn by watching what happens.
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/
Lesson III: If This Is True Then....
In this chapter we talk about Comparisons, Decisions and Truth. On of the most relevant job of a app is to make decisions. "Condition" will become a keyword for you.
Let us first have a look on a typical decision:
Global X=4
Global Y=3
If X=Y Then
Print "are equal"
' now any stupid reaction:
X=Y+1
Else
Print "are not equal"
' now any stupid reaction:
X=Y
Endif
Print "Now X=" + X
This part of a code checks if two variables are the same or not. Depending on the result it jumps into one of the two branches. At the EndIf both branches get back together and work both the rest of the code below EndIf
If....Then...Else...Endif divides the program flow into two branches related to the result of the check.
TRUE versus FALSE
Computers know two Conditions: TRUE or FALSE. Both can be the result of an comparision, but also a TRUE returned by a function is good enough for a decision.
f.e. AppTerminate() return True if the use wants to close the window. Or MouseHit() checks if the mouse button is pressed. So also this would be good for a IF...decision:
' example 1:
If MouseHit(1) Then
' do something when mouse button is pressed
Endif
' example 2:
If AppTerminate() Then
' do something when user wants to close the window
Endif
' example 3 nonsense but possible:
If TRUE Then
' do something always
Endif
MouseHit(1) checks whether the user has pressed LEFT mouse button
Now back to our first example:
Global X=4
Global Y=3
If X=Y Then
Print "are equal"
' now any stupid reaction:
X=Y+1
Else
Print "are not equal"
' now any stupid reaction:
X=Y
Endif
Print "Now X=" + X
As you can see inside the branches the X will be already changed into someting new. This is aloowed and does not linger affect the prior decision. In our example we make X unequal to Y in the case where it was equal. And we make it equal in the case, where it was unequal. Nonsense... but allowed. You can do whatever you want inside a IF-branch.
You can also combine more than one decision:
Global X=3
If X=3 Then
X=44
ElseIf X>20
Print "X is big!"
ElseIf X=44
Print "You will become millionaire now!"
Else
Print "none of them all"
Endif
Print "Now X=" + X
Don't forget the fact that in branch "3", after X becomes 44 the program flow jumps directly to the Endif. This means you will not reach the check against 44, although this code line is written below branch "3".
Now a task for you: How do you need to change the first code line...
X=3
...to achieve that the branch 44 can be reached and you will become millionaire?
Do you think...
x=44
... is the right answer? No! No number can reach the branch "44" because all numbers higher than 20 always jump
already to the branch "X is big" and will never reach the branch "44"
Challenge I: Change direction
Rewrite the little app in lesson two with a change: The rectangle should change its direction, when X reaches 400. Now the rectangle should move (vertical ) downwards.
Advanced Challenge II: Change direction again
Rewrite the little app in lesson two with two changes: The rectangle should change its direction, when X reaches 400. Now the rectangle should move downwards. When Y reaches 300 the rectangle should change the direction again: now moves horizontal to the right until it leaves the screen.
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/
.
Lesson IV: SuperStrict - A Good Coding Style
Please write code, that prevents bugs and is readable for others too. Also you should understand, what you have written, when you re-open your project one year later. BlitzMax helps you to keep to strict rules. This will prevent typos and bugs and save a lot of time for you.
So our standard template gets a new "look":
SuperStrict
Graphics 600,400
Global x:Int=5
Global t:String=" the result of X/600 ="
Repeat
Cls
DrawRect X,200,5,5
Local result:Double = x/600.0
DrawText t + result , 100 , 100
x = x+1
Flip
Until AppTerminate()
Print x
SuperStrict forces the user to define variables clearly. Is always the first line of every BlitzMax code!!!
Global x:Int=... defines a variable with name "x" as a INTEGER (whole number) variable
Global t:String=... defines a variable with name "t" as a STRING (text only) variable
Local result:Double =... defines a variable with name "result" as a DOUBLE-FLOAT (floating point number) variable
Global or Local?
GLOBAL defines a variable as everywhere accessible. This means everywhere in your code you can refer to this variable. The variable x is used before the loop, inside the loop and also after the loop. So you need to set it to GLOBAL.
LOCAL defines a variable as accessible only inside it's surrounding loop or function. This means outside the loop the variable is unknown. The variable result appears only inside the Repeat/Until-loop, so you can define it as LOCAL.
For beginners it is difficult to determine whether GLOBAL or LOCAL is right. So you need to know, that in your first steps is not important: Define all variables as GLOBAL until you know, why you would decide a certain variable as LOCAL. During the following lessons you will again and again get more informations about LOCAL use.
INT FLOAT STRING?
Define a variable as INT, if you are sure, that it will only contain whole numbers like 0, 333 or -2345.
Define a variable as DOUBLE, if you already know, that it will contain floating point numbers like 0.7 or 3.33 or -234.5.
Define a variable as STRING, if it will contain text and you will not use it in calculations. Text means also numbers or special letters like a question mark. Examples: "Hello" or "1-2-3" or "<!>"
INT = INTEGER
Integers can only contain whole numbers. If you add a floating point number the result will be a integer truncate and mathematical wrong:
Global x:Int=4
x = x + 4.5
' now x will be 8
INT's can contain numbers from -2Mio to +2Mio. If you need bigger number use the definition :LONG
DOUBLE = FLOATING POINT NUMBER
DOUBLE's can calculte with floating points. The result is because of "double precision" fast and exact.
In former times we often used FLOAT (a single precision floating point) but it is not that accurate:
Global tt:Float=123450
Print tt/12345.0
' you would expect the result is 10, but it is wrongly 4.69129229
STRING = TEXT
In BlitzMax Strings ca be "added", but this means the letters get connected:
Global a:String="01"
Global b:String="22"
Global c:String= a + b
' c is now "0123"
Challenge I: Stop at 234
Change the code above, that also X is diplayed in the sentence (and the result is displayed too), stop the app exactly at x=234 and compare your text with mine here:
the result of 234/600 = 0.39000001549720764
Is it exactly the same?
Challenge II: Sum of Floating Values
Calculate the sum of all results. Display this result as an integer. Stop at x=234 and compare the final result with my result: Is your result the same?
"the result of 234/600 = 45"
Do you see the same number?
Challenge III: Sum of all integer numbers
Write an app that outputs (PRINT) text lines like this. Each time the user presses any key the list should become one line longer:
0 + 1 = 1
0 + 1 + 2 = 3
0 + 1 + 2 + 3 = 6
0 + 1 + 2 + 3 + 4 = 10
0 + 1 + 2 + 3 + 4 + 5 = 15
0 + 1 + 2 + 3 + 4 + 5 + 6 = 21
...
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/
Lesson V: Moorhuhn Lets Do Our first Game
One of the biggest problem of beginners is not to understand the commands but the knowing how to solve a problem. Today we try to learn how to start a game, see the single steps and transfer them to code.
do you know the old "Moorhuhn" game from the 90th? A landscape, a funny chicken and somebody with a gun. The aim of the game is to shoot the chicken. Thats all. But how to start?
The template
We always start with this empty template:
SuperStrict
Graphics 800,600
Repeat
Cls
Flip
Until AppTerminate()
You can choose a different size of your window, but you should still be able to watch the OUTPUT window of the IDE when your game runs! We are not playing! We are developing.
Create a new folder for our project and save the code as "moor.bmx" into it.
The landscape
As long as you use your game only for your private home-use you can make use of nice landscape fotos in the internet. Or you take this free png in the attachment:
(https://www.syntaxbomb.com/index.php?action=dlattach;topic=8765.0;attach=5098;image)
Download it and save it as "land.png" into our new project folder next to the code.
SuperStrict
Graphics 800,600
Global Land:Timage = LoadImage("land.png")
Repeat
Cls
DrawImage Land , 0 , 0
Flip
Until AppTerminate()
Global Land:TImage beside the INTEGER, DOUBLE and STRING variable types there are a lot more "special" types. Here: A variable from type Timage can contain a Images.
LoadImage("...") loads a image file into a special TImage-variable.
DrawImage image , x, y draws the image at coordinates (x/y)
You should test each step we coded. So run now F5 to check whether the app starts. Starting your app as often as possible means, that if a problem occurs you always know, that it has to do with the last commands you wrote. It is no good idea to write dozends of code line without testing intermediate steps.
The moorhuhn
This works nearly the same. Find a chicken and add it to the code like before. If you do not find any picture you can also use placeholder for the moment. Often this is not a bad idea, because your team is not ready with the paintings but you already can continue coding. So I demonstrate this way now:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadImage("chicken.png")
Global cX:Int, cY:Int=300
Repeat
Cls
DrawImage Land , 0 , 0
cX = cX +2
'DrawImage Chicken , cX , cY
' comment out the original command, and use DrawRect instead:
DrawRect cX, cY, 60,40
Flip
Until AppTerminate()
As you see, we move the "chicken" from left to right by adding 2 to chicken-variable cX.
The crosshairs
We connect two "lines" with the mouse:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadImage("chicken.png")
Global cX:Int, cY:Int=300
Global mX:Int, mY:Int
Repeat
Cls
DrawImage Land , 0 , 0
cX = cX +2
'DrawImage Chicken , cX , cY
' comment out the original command, and use DrawRect instead:
SetColor 111,0,0
DrawRect cX, cY, 60,40
mX=MouseX()
mY=MouseY()
SetColor 255,255,255
DrawRect mX-25, mY-1,50,3
DrawRect mX-1, mY-25,3,50
'HideMouse
Flip
Until AppTerminate()
ShowMouse
MouseX() and MouseY() return the mouse coordinates into a variable
Setcolor R,G,B changes the Draw Color for all Draw... commands
HideMouse Hides the standard mouse icon
ShowMouse Shows the standard Mouse (here: before leaving the app )
We produce the lines of the crosshairs with DrawRect. As it has a length of 50pixels we have to start 25pixel left of the mouse to get a symetric design.
Color R=Red G=Green B=Blue
The Setcolor command changes the drawing color for the subsequent Draw... commands.You have to set three parameters to define a color: R G B. Colors are a combination of this three subcolors. Each value can be between 0 (dark)and 255 (bright). Here are some examples:
SetColor 255, 0 , 0 = red
SetColor 0 ,255, 0 = green
SetColor 0 , 0 ,255 = blue
SetColor 255,255, 0 = yellow
SetColor 255,255,255 = white
SetColor 1 , 1 , 1 = black
to be continued tomorrow....
Challenge I: Follow the Mouse
Write an app, where a rectangle slowly follows the mouse. Means if you move the Mouse quickly to the right, the rectangle follows, but slowly.
Challenge II: Dia-Show
Write an app, where the user can display fotos and switch on with pressing any key.
Challenge III: Nocturnal Hunt
Modify the Moorhuhn and change it into a "night-version". We are hunting in the dark
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/
Lesson VI: Moorhuhn Collision and SoundToday we learn how to outsource code lines from a main loop into a selfmade
Function() . This keeps our main loop short and clean. You will make some noise and kill some chicken.
Let's shoot at the poor animal. SuperStrict
...
Global Shot:TSound=LoadSound("fire.ogg")
...
Repeat
...
cX = cX +2
GunFire()
SetColor 255,255,255
DrawRect mX-25, mY-1,50,3
...
Until AppTerminate()
...
Function GunFire:Int()
mX=MouseX()
mY=MouseY()
If MouseHit(1)
PlaySound Shot
EndIf
Return FALSE
End Function
All the code related to Mouse and shooting is now outsourced. This can be done by creating a new function. In the original place we "call" the new function. Each time, when the program flow reaches that point, it will jump into the function and process all code lines there. Then it will return to the prior point to continue in the main loop.
Functions can use GLOBAL variables. This is not a good coder style, but it is possible, so we do it for now. As you already know from the function
AppTerminate() or from function
MouseX() the functions can return values. So can our selfmade function too. This is the reason why the function-name contains the appendix
:Int. From the "outside" the function looks like an INTEGER value.
Function Name:Int() returns an INT value to the calling code line
Global Shot:TSound TSound is a special variable Type, that can contain sounds
LoadSound("...") loads a sound from disk and returns it to a variable of type TSounds
PlaySound xyz Plays the sound stored in a variable xyz
Return xyz returns the content of the variable xyz to the calling line. Also expressions like FALSE are possible values
Search for a "Shoot"-noise in the internet or use the sound here in attachment. Copy it into the project folder. Switch your speaker on, start the app with F5... and now it is getting loud.
CollisionFinding out if two coordinates are at the same place is very easy. We only have to test whether X and Y in both coordinates are the same:
If (mX=cX) AND (mY=cY) Then
' means collision
Return TRUE
Endif
But if you want to find out, whether a coordinate is inside a rectangle it is more complicate. The mX can be:
left of the rectangle
between cX and the right end of the rectangle (cX+60)
right of the rectangle.
Now try to express this in a "condition" code line.
We are interested in case 2: Between. If you do not know how to transfer "between" to a IF-code line perhaps you could tell me case 1 and 3?
If mX<cX Then
' outside left
Return FALSE
Endif
If mX>(cX+60) Then
' outside right
Return FALSE
Endif
' the same for the y-coordinates:
If mY<cY Then
' outside above
Return FALSE
Endif
If mY>(cY+60) Then
' outside below
Return FALSE
Endif
' the program flow reaches this point only in those cases,
' where the coordinates are inside the rectangle.
' That is, what you have been looking for !!!
Return TRUE
There are always several ways to get to a solution! Of course you can code it directly:
If (mX>cX) And (mx<cX+60)
If (mY>cY) And (mY<cY+40)
' collision
Return True
EndIf
EndIf
Now the reaction in the main loop:
If GunFire() =True Then
cX=-100
Points=Points+1
EndIf
this moves the chicken to the very left, so that it disappears
We count the Hits
The whole app:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadImage("chicken.png")
Global Shot:TSound=LoadSound("fire.ogg")
Global cX:Int, cY:Int=300
Global mX:Int, mY:Int
Global Points:Int
Repeat
Cls
DrawImage Land , 0 , 0
'DrawImage Chicken , cX , cY
' comment out the original command, and use DrawRect instead:
SetColor 111,0,0
DrawRect cX, cY, 60,40
cX = cX +2
If GunFire() =True Then
cX=-100
Points=Points+1
EndIf
SetColor 255,255,255
DrawRect mX-25, mY-1,50,3
DrawRect mX-1, mY-25,3,50
'HideMouse
DrawText "Points: " + points, 700,550
Flip
Until AppTerminate()
ShowMouse
Function GunFire:Int()
mX=MouseX()
mY=MouseY()
If MouseHit(1)
PlaySound Shot
If (mX>cX) And (mx<cX+60)
If (mY>cY) And (mY<cY+40)
' collision
Return True
EndIf
EndIf
EndIf
Return False
End Function
Challenge I: Play a drum machineIn the attachment you find two drum sound: a SnareDrum SD2.ogg and a BassDrum BD2.ogg. With this you can build a drum computer. Draw two circles on the screen. When the user clicks in the left the BassDrum sound plays, when clicking in the right the SnareDrum sound plays.
Challenge II: Forest Background SoundAdd a forest background sound to the Moorhuhn game. You will find several examples in the internet. As you need a WAV or a OGG file, use a internet file converter to convert the file as you need it.
Challenge III: Painting AppWrite an app, where the user can draw on the screen, switch colors and clear the screen when pressing a "NEW" button.
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/
Lesson VII: Moorhuhn Random Movements
In our last version the chicken had no chance. It always comes from the same corner, goes the same way and same speed. Today i'll stand by the moorhuhn... give them a chance..
To make the game varied and entertaining we change the moorhuhn's moves by random.
We replace the old movement by a new functionname:
main loop:
...
' some new chicken variables:
Global cX:Double=9999, cY:Double, cAddX:Double, cAddY:Double
Repeat
Cls
DrawImage Land , 0 , 0
...
SetColor 111,0,0
' -------- n e w p a r t ---------------
TheChicken()
Print "c:" + Int(cx) + " " + Int(cy)
' ----------------------------------------
DrawRect cX, cY, 60,40
If GunFire() =True Then
cx=9999 ' <--- NEW
Points=Points+1
EndIf
...
Until AppTerminate()
Function TheChicken()
End Function
and at the end of the code we add a the new empty function function:
We have to think about two situations.
1. send a new chicken when it is was shot down or flew outside the screen
2. move the chicken as long as it is visible on the screen.
Function TheChicken()
If (cY<-101) Or (cY>601) Or (cX <-201) Or (cX>1001)
' chicken is out of screen
Else
' chicken is visible
Endif
End Function
Perhaps you ask yourself, why we not simply use the screen-dimension 800 and 600 in our condition?
If (cY<0) Or (cY>600) Or (cX <0) Or (cX>800)
There are several reasons. A chicken is already (or still) visible at position -50, when it's width is 60!!! So we need to do this...
If (cY<-61) Or (cY>601) Or (cX <-61) Or (cX>801)
...to guarantee that is is really gone.
But additonal we do not want to start the next chicken in the very moment, when the last has gone. So we start them at a distance of -200 to the left and +200 to the right. Now the next chicken needs a moment until it reaches the screen border. But this means that we cannot determine a "gone chicken" by simply determine a position left of 0, because this could also be a "coming chicken". So we enlarge the area to:
If (cY<-101) Or (cY>601) Or (cX <-201) Or (cX>1001)
To force a fresh chicken at the game start or when we shot it down, we simply set the chicken far outside the screen:
Global cX:Double=9999, cY:Double, cAddX:Double, cAddY:Double
If GunFire() =True Then
cx=9999
This forces the function TheChicken() to start a new chicken
to observe what happens with our variables cX and cY we log them to the DEBUG (OUTPUT) window of BlitzMax
Print "c:" + Int(cx) + " " + Int(cy)
Use this PRINTs extensively during develpoment. You will often save a lot of time in finding bugs by recognizing, that the variables are not doing, what you expected.
Starting A Chicken
Function TheChicken()
If (cY<-100) Or (cY>600) Or (cX <-201) Or (cX>1001)
'send a new the chicken
cY=Rand(100,500)
If Rand(0,1)=0
'coming from right
cX=1000
cAddX=Rnd(-7 , -4)
Else
'coming from left
cX=-200
cAddX=Rnd(4 , 7)
EndIf
cAddY=Rnd(-3 , +3)
Else
....
Rand(From, To) A function that returns an INTEGER random number in the range of FROM to TO.
Rand(10,20)
produces a random number between 10 and 20.
Rnd(From, To) A function that returns an random DOUBLE FLOATING POINT number in the range of FROM to TO.
Rnd(0.3 , 1.8)
produces a random number between 0.3000 and 1.7999.
If Rand(0,1)=0 ' 50:50 joker
'coming from right
cX=1000
cAddX=Rnd(-7 , -4)
A chicken coming from the right side will start at x=1000pixels and has a negative speed between 4 and 7 pixels.
Else
'coming from left
cX=-200
cAddX=Rnd(4 , 7)
When the chicken is coming from the left side it will start at x=-200pixels and has a positive speed between 4 and 7 pixels.
To get also diagonal movements we also define a light y-speed:
cAddY=Rnd(-3 , +3)
The Movement
Function TheChicken()
If (cY<-100) Or (cY>600) Or (cX <-201) Or (cX>1001)
...
Else
cX = cX + cAddX
cY = cY + cAddY
EndIf
End Function
Challenge I: Display the outer space
Write an app that display something like a look to the sky with a lot of stars.
Challenge Ib: Produce Snow
As an variant write now an app that display something like moving snow.
Challenge II: Roll A Dice
Write an app that display a new dice result (1-6) each time the user presses any key.
Challenge IIb: Show three dices
Now expand the app to display the last three dices the user rolled.
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/
Lesson VIII: Moorhuhn Animation
today we remove the stupid rectangle and replace it by a "real" flying chicken: You will learn how to draw animated Sprites.
Preparation
For animations we need a PNG-file, which contains a long strip with a number of "frames". Each frame contains the "same" picture, but shows a different step of the animation.
Step 1:
I found a GIF-file for you with a nice chicken. You need to download it and save it into our project folder with the exact same name the browser offers: moorhuhn01.gif
http://www.ollis-page-online.de/specials/moorhuhn_x/moorhuhn01.gif
Step 2:
BlitzMax cannot process this file-typ. So we need an app, that transforms this into our PNG-file format. In the attachment of this post you find a BMX-file that can do this. It is a automated app, which automatically transforms moorhuhn01.gif to chicken.png.
Step 3:
Copy this BMX-files to our project folder, load it into BlitzMax and start it once with F5. It will start, run and open a window. If you can see the animated chicken, everything was processed perfect. Close the app. You will find a new file chicken.png next to the moorhuhn01.gif. Now you can close BlitzMax and return to our project. Please do not hand over or publish the image-files because of the copyright.
Step 4:
For a better understanding you can open the PNG file as a regular TImage or in a paint-app to see that there are 12 frames with only little differences. It is like in a thumb cinema: When we display the frames very fast it will look like a movement.
This was the code version of lesson VII:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadImage("chicken.png")
Global Shot:TSound=LoadSound("fire.ogg")
Global cX:Double=9999, cY:Double, cAddX:Double, cAddY:Double
Global mX:Int, mY:Int
Global Points:Int
Repeat
Cls
DrawImage Land , 0 , 0
'DrawImage Chicken , cX , cY
' comment out the original command, and use DrawRect instead:
SetColor 111,0,0
TheChicken
Print "c:" + Int(cx) + " " + Int(cy)
DrawRect cX, cY, 60,40
If GunFire() =True Then
cx=9999
Points=Points+1
EndIf
SetColor 255,255,255
DrawRect mX-25, mY-1,50,3
DrawRect mX-1, mY-25,3,50
'HideMouse
DrawText "Points: " + points, 700,550
Flip
Until AppTerminate()
ShowMouse
Function GunFire:Int()
mX=MouseX()
mY=MouseY()
If MouseHit(1)
PlaySound Shot
If (mX>cX) And (mx<cX+60)
If (mY>cY) And (mY<cY+40)
' collision
Return True
EndIf
EndIf
EndIf
Return False
End Function
Function TheChicken()
If (cY<-100) Or (cY>600) Or (cX <-201) Or (cX>1001)
'reset the chicken
cY=Rand(100,500)
If Rand(0,1)=0
'coming from right
cX=1000
cAddX=Rnd(-7 , -4)
Else
'coming from left
cX=-200
cAddX=Rnd(4 , 7)
EndIf
cAddY=Rnd(-3 , +3)
Else
cX = cX + cAddX
cY = cY + cAddY
EndIf
End Function
Now we change to animated Images:
We need only four new lines:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadAnimImage("chicken.png",140,140,0,12) ' <------- NEW LINE
Global AnimStep:Int ' <------- NEW LINE
Global Shot:TSound=LoadSound("fire.ogg")
...
Repeat
Cls
DrawImage Land , 0 , 0
DrawImage Chicken , cX , cY, AnimStep ' <------- NEW LINE
AnimStep = (AnimStep +1) Mod 12 ' <------- NEW LINE
TheChicken
If GunFire() =True Then
.....
LoadAnimImage( URL, Width, Height, 0, Count) loads a stripped PNG into a variable of type TImage.
You have to declare the Height and Width of a single frame. The third parameter 0 means animation starts from frame 0 (most left frame). The 12 is our number of frames.
If you do not know exactly the frame-sizes you could ask for them with this trick:
...
Global tempImage:TImage = LoadImage("chicken.png")
Global H:Int = tempImage.Height
Global W:Int = tempImage.Width/12
Print "Frames size: Width=" + W + " Height=" + H
Global Chicken:TImage = LoadAnimImage("chicken.png", W , H , 0 , 12)
Image.Width() return the Width of a loaded Image
Image.Height() return the Height of a loaded Image
Some variables have additional "properties", you can aks for them with adding a dot and an appendage. Later you will learn that these properties are called FIELDS in BlitzMax. More later....
DrawImage Chicken , cX , cY, AnimStep
Here you see, that the DrawImage function can have 4 parameters. This forth parameter defines which of the 12 frames will be displayed.
AnimStep = (AnimStep +1) Mod 12 The MOD function works like "remainder" in Mathematics. It divides numbers, but it does not show the result, but the remainder.
13 divided by 5 is 2 remainder 3
14 divided by 5 is 2 remainder 4
15 divided by 5 is 3 remainder 0
....
In coding we use this for circular jobs. Each time, when the variable AnimStep reaches 12 it will be reseted to 0.
AnimStep = (AnimStep +1) Mod 12
You could also do this in four code lines:
AnimStep = AnimStep +1
If AnimStep =12
AnimStep=0
Endif
Now try out the new fantantic flying chicken game !
everything ok? No?
We Have To Mirror Vertically
Repeat
Cls
DrawImage Land , 0 , 0
If cAddX>0 ' <------- NEW LINE
SetScale -1,1 ' <------- NEW LINE
EndIf
DrawImage Chicken , cX , cY, AnimStep
SetScale 1,1 ' <------- NEW LINE
SetScale X,Y is a function with two "jobs". We can zoom single objects and define independent zooms for the X and the direction. And we can change drawing direction by using negativ values.
SetScale 2 2 ' object is displayd with double size
SetScale 0.5 , 0.5 ' object is displayd with half size
SetScale 2 , 0.5 ' object is horzontaly streched
SetScale -1 , 1 ' object is verticaly mirored
SetScale 1 , -1 ' object is horizontaly mirored
SetScale 1 , -1 ' object looks like turned 180°
...
Dont' forget to reset the Scaling with a SetScale 1,1 after the desired objects are drawn.
Challenge I: Show them all
Write an app that shows all 12 Frames of the animated chicken at the same time sorted in a grid of 4x3 pictures
Challenge Ib: Slow Motion
Write an app that shows a "slow motion" of the animated chicken. The "movie" is frozen and continues only one frame each time the user press any key.
Challenge II: Childlike Division
Write an app that displays the result of a division (of two random numbers) not as floating point but in a childlike way: "whole number quotient" followed by "remainder": 26 : 7 = 3 remainder 5
No Challenge
If you now play the game you can observe a bug. But can you find out why it happens? (please do not write a solution to the forum). How could we prove what happens? so next chapter will be : "How to find bugs"
more tomorrow....
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/
Lesson IX: How To Find Bugs
One of the hardest jobs is to find a selfmade bug in your code. Of course you already did find out, that the gun does not shoot the chicken anymore. But what happened? At first it worked.. and now with the animated chicken it fails....
Additional you will learn more about "functions" and BlitzMax's "commands"
Stop The Game
A helpful tool is to interrupt the games movements and "freeze" the game. We can simply do this by prevent all addition to movement related variables. When you press a shortcut the chicken will freeze:
...
Function TheChicken()
If KeyDown(KEY_S) Return 'check the keyboard key S and end the function
...
We need only one line to hold the chicken in the middle of the screen. Now you can shoot as often as you like to find out what happens.
Return stopps the function and returns immediately to the calling code line.
KeyDown(KEY_CODE) return the state of a keyboard key. You have to know the KEY_CODE of the key you want to check. The KEY_CODEs are listed in BlitzMax at (the right menu) HOME-USER INPUT-KEYCODES. Or guess them, the names are intuitive:
KEY_A ... KEY_Z for ABC-keys,
KEY_1..KEY_9 for the numbers
KEY_ESCAPE ESC-button
KEY_LEFT left cursor key
....
Create New Drawing Commands
To make visible how our gun checks the hits, we will show it's sensitive area on the screen. Therefore we use a new BlitzMax Drawing command, that displays non-filled rectangles. So... what is the difference between BlitzMax commands and "our" (selfmade) functions? None! Both are functions(). Function can get parameters and can return results. A function like
DrawRect 100,100,200,40
has 4 parameters: X Y Width and Height. The function will need this 4 values to build the rectangle on the screen.
We can do the same:
DrawNonFilledRect 100,100,200,40
will Draw a non filled rectangle at 100,100 with width=200 and height=40. As there is no BltzMax function for that, we write our own:
Function DrawNonFilledRect(X:Int, Y:Int, Width:Int, Height:Int)
DrawLine x,y,x+width,y
DrawLine x,y,x,y+Height
DrawLine x+width,y,x+width,y+height
DrawLine x,y+Height,x+width,y+Height
End Function
We draw 4 lines and calculate the start and the end points
and here the same with a more structured text:
Function DrawNonFilledRect(X:Int, Y:Int, Width:Int, Height:Int)
DrawLine x , y , x+width , y ' top line
DrawLine x , y , x , y+Height ' left line
DrawLine x+width , y , x+width , y+height ' right line
DrawLine x , y +Height , x+width , y+Height ' bottom line
End Function
BlitzMax allows as many SPACE characters in code lines as you need to display the code comfortable for you
and here the same with a complete different code:
Function DrawNonFilledRect(X:Int, Y:Int, W:Int, H:Int)
Local r:Int = X + W 'right
Local b:Int = Y + H 'bottom
DrawRect x , y , W , 1 ' top line
DrawRect x , y , 1 , H ' left line
DrawRect r , y , 1 , H ' right line
DrawRect x , b , W , 1 ' bottom line
End Function
Here we coded a solution with DrawRects instead of DrawLines. You decide how to solve a problem.
Local defines a variable only existing inside a function.
If you need additional variables inside your functions you always declare them LOCAL. This means they do not exist outside this function. They are temporary. This enables to use the the same name for variables inside and outside the function or in a third function. And they all do not affect each other.
Now add one of the DrawNonFilledRect() functions to your game.
And change our gun-Function to:
Function GunFire:Int()
mX=MouseX()
mY=MouseY()
If KeyDown(KEY_S) ' <-- NEW CODE LINE
SetColor 255,0,0 ' <-- NEW CODE LINE
'make the sensitive area visible: ' <-- NEW CODE LINE
DrawNonFilledRect(cX, cY, 60, 40) ' <-- NEW CODE LINE
EndIf ' <-- NEW CODE LINE
If MouseHit(1)
PlaySound Shot
If (mX>cX) And (mx<cX+60) ' <-- sensitive area
If (mY>cY) And (mY<cY+40) ' <-- sensitive area
' collision
Return True
....
and start the game with F5. Press <S> when the chicken is in the middle of the screen
Now We See...
Now we can see, that the chicken is not at that place, where the shoot-sensitive area thinks it is. This happened, when we replaced the early Chicken-Rectangle with the Chicken-Image. The Chicken-Image has complete different dimensions. But we still look for a 60x40pix rectangle.
It is never a good idea to code absolute numbers in games. Better is to use variables or functions which know the current dimensions.
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadAnimImage("chicken.png",140,140,0,12)
Global cWidth:Int = Chicken.Width ' <------- New LINE
Global cHeight:Int = Chicken.Height ' <------- New LINE
.....
Function GunFire:Int()
mX=MouseX()
mY=MouseY()
If KeyDown(KEY_S)
SetColor 255,0,0
'make the sensitive area visible:
DrawNonFilledRect(cX, cY, cWidth, cHeight) ' <-- NEW CODE LINE
EndIf
If MouseHit(1)
PlaySound Shot
If (mX>cX) And (mx<cX+cWidth) ' <-- NEW CODE LINE
If (mY>cY) And (mY<cY+cHeight) ' <-- NEW CODE LINE
Image.Width knows the image's width.
Image.Height knows the image's height.
Change the code and test with KEY S
Now the rectangle is around the chicken, but the size is too big. The reason is, that the chicken.png is bigger than the chicken itself. So now we should adjust the true size:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadAnimImage("chicken.png",140,140,0,12)
Global cOffsetX:Int = 20 ' <------- New LINE
Global cOffsetY:Int = 20 ' <------- New LINE
Global cWidth:Int = Chicken.Width -50 ' <------- New LINE
Global cHeight:Int = Chicken.Height -50 ' <------- New LINE
....
Function GunFire:Int()
mX=MouseX()
mY=MouseY()
If KeyDown(KEY_S)
SetColor 255,0,0
'make the sensitive area visible:
DrawNonFilledRect(cX+cOffsetX, cY+COffsetY, cWidth, cHeight) ' <-- NEW CODE LINE
EndIf
If MouseHit(1)
PlaySound Shot
If (mX>cX+cOffsetX) And (mx<cX+cOffsetX+cWidth) ' <-- NEW CODE LINE
If (mY>cY+cOffsetY) And (mY<cY+cOffsetY+cHeight) ' <-- NEW CODE LINE
' collision
Return True
...
Change again the code and test with KEY S
Now the rectangle is closer around the chicken, but the chicken coming from the left has a completely wrong sensitive area. This is because we are drawing the (left coming) chicken with SetScale(-1,1). This mirrors the chicken at its left bounds. There is a BlitzMax function to force images to use the middle of the image as a reference point.
MidHandleImage chicken
MidHandleImage move the reference point of an image to its center
but this needs again a new findout of the dimensions:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadAnimImage("chicken.png",140,140,0,12)
MidHandleImage chicken
Global cOffsetX:Int = -40 ' <------- New LINE
Global cOffsetY:Int = -50 ' <------- New LINE
Global cWidth:Int = Chicken.Width -50
Global cHeight:Int = Chicken.Height -50
...
And this is the complete code:
SuperStrict
Graphics 800,600
Global Land:TImage = LoadImage("land.png")
Global Chicken:TImage = LoadAnimImage("chicken.png",140,140,0,12)
MidHandleImage chicken
Global cOffsetX:Int = -40 ' <------- New LINE
Global cOffsetY:Int = -50 ' <------- New LINE
Global cWidth:Int = Chicken.Width -50 ' <------- New LINE
Global cHeight:Int = Chicken.Height -50 ' <------- New LINE
Global AnimStep:Int
Global Shot:TSound=LoadSound("fire.ogg")
Global cX:Double=9999, cY:Double, cAddX:Double, cAddY:Double
Global mX:Int, mY:Int
Global Points:Int
Repeat
Cls
DrawImage Land , 0 , 0
If cAddX>0
SetScale -1,1
EndIf
DrawImage Chicken , cX , cY, AnimStep ' <------- NEW LINE
SetScale 1,1
AnimStep = (AnimStep +1) Mod 12 ' <------- NEW LINE
TheChicken
If GunFire() =True Then
cx=9999
Points=Points+1
EndIf
SetColor 255,255,255
DrawRect mX-25, mY-1,50,3
DrawRect mX-1, mY-25,3,50
HideMouse
DrawText "Points: " + points, 700,550
Flip
Until AppTerminate()
ShowMouse
Function GunFire:Int()
mX=MouseX()
mY=MouseY()
If KeyDown(KEY_S) ' <-- NEW CODE LINE
SetColor 255,0,0 ' <-- NEW CODE LINE
'make the sensitive area visible:
DrawNonFilledRect(cX+cOffsetX, cY+COffsetY, cWidth, cHeight) ' <-- NEW CODE LINE
EndIf
If MouseHit(1)
PlaySound Shot
If (mX>cX+cOffsetX) And (mx<cX+cOffsetX+cWidth) ' <-- NEW CODE LINE
If (mY>cY+cOffsetY) And (mY<cY+cOffsetY+cHeight) ' <-- NEW CODE LINE
' collision
Return True
EndIf
EndIf
EndIf
Return False
End Function
Function TheChicken()
If KeyDown(KEY_S) Return ' <-- NEW CODE LINE
If (cY<-100) Or (cY>600) Or (cX <-201) Or (cX>1001)
'reset the chicken
cY=Rand(100,500)
If Rand(0,1)=0
'coming from right
cX=1000
cAddX=Rnd(-7 , -4)
Else
'coming from left
cX=-200
cAddX=Rnd(4 , 7)
EndIf
cAddY=Rnd(-3 , +3)
Else
cX = cX + cAddX
cY = cY + cAddY
EndIf
End Function
' NEW CODE LINES:
Function DrawNonFilledRect(X:Int, Y:Int, W:Int, H:Int)
Local r:Int = X + W 'right
Local b:Int = Y + H 'bottom
DrawRect x , y , W , 1 ' top line
DrawRect x , y , 1 , H ' left line
DrawRect r , y , 1 , H ' right line
DrawRect x , b , W , 1 ' bottom line
End Function
Challenge I: Number Input Function
Write an app where the user can use the number keys of his keyboards to enter numbers. Additional he can use RETURN to start a new number
Challenge I: Write a Gadget
Expand the Challenge I to an universal function: A box ("window") appears and the user can enter a number. On RETURN the box dissappears and returns the number to the main app. Additional you can add more gadgets like. Buttons, Sliders, etc..
Challenge III: DrawTriangle
Write a function that draws outline triangles. If you wantyou can try to fill them.
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/
Lesson X: Time related action in games
Today you will learn how to do things exactly in a given time, speed or at a given moment. At the moment our games speed is related to the performance of the computer. As faster the computer is as faster runs your game. This is not what we want.
Here is a simple demo, that show three element doing something on the screen. Copy the code and test it several times to get a feeling how fast this all happens:
SuperStrict
Graphics 800,600
Global TextVisible:Int
Global X:Int
Global Y:Int
Repeat
Cls
' a blinking text:
TextVisible = Not TextVisible
If TextVisible=True
DrawText "This is a blinking text", 100,100
EndIf
' a moving rectangle:
X=X+1
DrawRect x,200,60,30
' a falling ball:
Y=Y+10
DrawOval 500,Y,30,30
Flip
Until AppTerminate()
XYZ = Not XYZ Turns a condition to the opposite. If XYZ was TRUE, now it becomes FALSE, if it was FALSE it returns to TRUE. This enable to switch between two states and is often used to switch ON/OFF or TRUE/FALSE or FORWARD/BACKWARD, etc...
Now simply change the Flip with a Flip 0 to feel what could happen if the performance is higher:
...
DrawOval 500,Y,30,30
Flip 0
Until AppTerminate()
Flip 0 rises the speed of our app to maximum performance. You need not to know what happens, but you can see, that the performance of an app can vary. Go back to old Flip.
So we need a controller, that manage actions on the screen time-based.
...
Global TextVisible:Int
...
Global Future:Int=MilliSecs() + 500
Repeat
Cls
' a blinking text:
If Future<MilliSecs()
TextVisible = Not TextVisible
Future= MilliSecs() + 500
EndIf
If TextVisible=True
DrawText "This is a blinking text", 100,100
EndIf
...
Millisecs() returns the time in milliseconds since we started the computer. This is often used to store a "Now"-moment. Later you can compare it with a next Millisecs() and measure the time that has passed. Here we use it to set a variable Future:INT to Now+500msec
In the main loop we check whether Future is still bigger than "Now". 500msec later it happens: Millisecs() becomes bigger than Future and the variable TextVisible swaps from TRUE to FALSE. At the same moment we set again Future to "Now"+500msec for a next turn.
Now test how the text is blinking. Replace the 500 with several values to get various blinking speeds.
We can do the same with the moving rectangle
...
Global Future:Int=MilliSecs() + 500
Global RectFuture:Int=MilliSecs()
Repeat
Cls
...
If RectFuture<MilliSecs()
X=X+1
RectFuture= MilliSecs() + 50
EndIf
...
DrawRect x,200,60,30
[code=BlitzMax]
At the beginning the rectangle will start immediately, then it will add X=X+1 every 50msec
now lets fall the ball after 4sec:
...
Global Future:Int=MilliSecs() + 500
Global RectFuture:Int=MilliSecs()
Global BallFuture:Int=MilliSecs()+ 4*1000
Repeat
Cls
...
If BallFuture<MilliSecs()
Y=Y+10
BallFuture= MilliSecs() + 15
EndIf
...
DrawOval 500,Y,30,30
[code=BlitzMax]
Challenge I: Speed Chicken
Now try to adjust the speed of the animation of the chicken in our Moorhuhn game.
Challenge II: Traffic Lights
Try to code a traffic light that changes the colors every 5sec from RED to YELLOW to GREEN to YELLOW and back to RED
Challenge III: Drum Computer
Try to code a drum-computer playing a rock- rhythm
Instrument: | BD SD BD BD SD |
Count: | 1 + 2 + 3 + 4 + |
I added two Drumsounds (see attachment)
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/
Lesson XI: Sinus and Cosinus in GamesFor moving objects in free directions on the screen we need to use rotations and the two related function
Sin() und
Cos(). No fear! We only use them, no need for understanding...
Both function enable us to calculate the X- and the Y-part of a movement from a degree-based "direction".
Things getting very easy when we define:
- direction 0° is "to the right"
- direction 90° is "to the bottom"
- direction 180° is "to the left"
- direction 270° is "to the top"
...and...
- Cos() is always used for movements in x-direction , means "horizontal" part
- Sin() is always used for movements in y-direction , means "vertical" part.
But let us watch a first example:
SuperStrict
Graphics 800,600
Global X:Double=400, Y:Double=300
Repeat
Cls
X = X + Cos(0)
Y = Y + Sin(0)
DrawOval X,Y,30,30
Flip
Until AppTerminate()
Cos:Double(Angle) returns a x-value between -1 and 1 related to the angle (0°-360°)
Sin:Double(Angle) returns a y-value between -1 and 1 related on the angle (0°-360°)
You see that both functions together generate a movement to the right when the angle=0°. If you now exchange the 0 with a 90 the movement will change to "downwards". This works for all angles between 0° and 360°
SuperStrict
Graphics 800,600
Global X:Double=400, Y:Double=300
Global Degree:Int=45
Repeat
Cls
X = X + Cos(Degree)
Y = Y + Sin(Degree)
DrawOval X,Y,30,30
Flip
Until AppTerminate()
Now do you experiments with various values between 0° and 360°
To rotate the object into the same direction we need a new command:
...
Repeat
Cls
SetRotation Degree
DrawRect 400,300,100,30
Flip
Until AppTerminate()
SetRotation(Angle) changing the BlitzMax drawing direction
Now we can combine both to something like a "moving car"
SuperStrict
Graphics 800,600
Global X:Double=400, Y:Double=300
Global Degree:Int=145
Repeat
Cls
X = X + Cos(Degree)
Y = Y + Sin(Degree)
SetRotation Degree
DrawRect X,Y,60,30
Flip
Until AppTerminate()
We connect a "steering wheel" with the mouse-wheel:
SuperStrict
Graphics 800,600
Global X:Double=400, Y:Double=300
Global Degree:Int=145
Repeat
Cls
Degree=degree + MouseZSpeed()*4
X = X + Cos(Degree)*2
Y = Y + Sin(Degree)*2
SetRotation Degree
DrawRect X,Y,30,15
Flip
Until AppTerminate()
MouseZSpeed:Int() returns -1 or +1 related to movements of the middle mouse wheel
Now we can exchange the DrawRect with the image of a car (see attachment) and care about the bounds od the screen, where the car should stop:
SuperStrict
Graphics 800,600
Global Car:TImage=LoadImage("car.png")
MidHandleImage Car
Global X:Double=400, Y:Double=300
Global lastX:Double, lastY:Double
Global Degree:Int=290
Repeat
Cls
Degree=degree + MouseZSpeed()*4
X = X + Cos(Degree)*2
Y = Y + Sin(Degree)*2
If X<30 Or x>770 Or Y<30 Or Y>570
X = lastX
Y = lastY
Else
lastX = X
lastY = Y
EndIf
SetRotation Degree
DrawImage Car,X,Y
Flip
Until AppTerminate()
Challenge I: Car Racing: Speed and CourseTry to add a background image, which shows a top-down-view of a racing course. Try to add a accelerator. When the user press the "S" button the car gets faster, pressing "B" works like a break.
Challenge II:Ping PongTry to code a ping-pong-game: A ball plays back and forth while the mouse moves a racket to turn the ball back
Challenge III: Outline CircleTry code a function that draws a not filled circle. You need a formula like this and a For/Next loop over 360°:
X = middleX + Cos(Angle)* Radius
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/
Lesson XII: For/Next-Loops
In computer programs we often need to do the same action for several times. Therefore we do not need to write the commands a dozend time, but we can do this in a counting loop:
For Local I:Int=1 to 10
Print "Hello. This is round " + i
Next
For I=Start To End defines the beginning a code loop with a fixed number of rounds. You need to define a counting variable (often called "i", often an INTEGER) and a first value, where i should start. Third parameter is the final value. When i reaches this, the loop will be done for a last time but not repeated any longer.
Next defines the end of the code loop. Now i gets incremented and the program jumps back to the code line below For... or continues the regular program flow,when i is bigger than the final value.
For I=1 to 10
will beginn with i=1 and loop 10 times until i=10
For I=10 to 15
will beginn with i=10 and loop 6 times until i=15. Yes! 6 times: 10...11...12...13...14...15
N=3
For I=0 to N
will beginn with i=0 and loop 4 times until i=3: 0...1...2...3
Practical use
We paint staves:
Graphics 800,600
Repeat
Cls
For Local i:Int=1 To 5
DrawLine 50, 100+i*10, 750, 100+i*10
Next
Flip
Until AppTerminate()
Or A Bundle Of Staves:
Graphics 800,600
Repeat
Cls
For Local j:Int= 1 To 4
For Local i:Int=1 To 5
DrawLine 50, (j*100)+(i*10), 750, (j*100)+(i*10)
Next
Next
Flip
Until AppTerminate()
Here two loops are nested to draw a complex design. The outer loop turns 4 times to draw the rows, the inner loop draws 5 times the lines within one row.
Some Confetti:
Graphics 800,600
Cls
For Local i:Int=1 To 1000
SetColor Rand(255), Rand(255), Rand(255)
DrawOval Rand(800), Rand(600),11,11
Next
Flip
WaitKey
With For/Next you can paint thousands or tenthousands of objects.
A Pseudo-3D-Scene:
Graphics 800,600
SetClsColor 88,55,0
Repeat
Cls
' 3D Z-lines:
SetColor 111,151,1
For Local i:Int=0 To 180 Step 10
DrawLine 400, 250, 400+Cos(i)*800, 250+Sin(i)*600
Next
' 3D X-Lines:
Local y:Double =3
For Local i:Int=1 To 30
y=y*1.2
DrawLine 0, 250+y, 800, 250+y
Next
' sky:
For Local i:Int=0 To 255
SetColor i,i,155
DrawLine 0,i, 800, i
Next
Flip
Until AppTerminate()
SetClsColor R,G,B defines the background color
Here we have again two nice ways to use For/Next:
- Draw rays comming from the middle (Z-lines)
- Draw a color gradient sky by often changing the SetColor together with a lot of DrawLines
Road-Movie
Here we draw a piece of road 8 times to get a whole road.
Graphics 800,600
Global Road:TImage=LoadImage("road.png")
MidHandleImage road
SetClsColor 0,88,0
Global StreetWidth:Int = 134
Repeat
Cls
For Local I:Int=0 To 7
DrawImage road,i*StreetWidth, 300
Next
DrawImage road,100, 100
Flip
Until AppTerminate()
Now we add a movement:
Graphics 800,600
Global Road:TImage=LoadImage("road.png")
MidHandleImage road
SetClsColor 0,88,0
Global StreetWidth:Int = 134
Global ScrollX:Int
Repeat
Cls
ScrollX = (ScrollX-2) Mod StreetWidth
For Local I:Int=0 To 7
DrawImage road, i*StreetWidth + ScrollX , 300
Next
DrawImage road,100, 100
Flip
Until AppTerminate()
To understand what happens, set the StreetWidth to 140 and reduce For/Next to 6 loops:
Global StreetWidth:Int = 140 '<----CHANGE HERE
....
Repeat
....
For Local I:Int=0 To 6 '<----CHANGE HERE
... and now start F5. Each image has a width of 134. We draw 8 streets (too many), so one street is always outside the right screen bound. Each Flip we move the starting point for the drawing ScrollX 2 to the left: All streets will be drawn 2pixel more to the left. The 8th street will appear slowly on the right bound. Exactly when Scroll is -134 we set it to 0 with the MOD command and the procedure starts again.
At last we add a car:
Graphics 800,600
Global Road:TImage=LoadImage("road.png")
Global Car:TImage=LoadImage("car.png")
MidHandleImage road
SetClsColor 0,88,0
SetBlend alphablend
Global ScrollX:Int, CarY:Double, Add:Double
Global StreetWidth:Int = 134
Repeat
Cls
ScrollX = (ScrollX-2) Mod StreetWidth
For Local I:Int=0 To 7
DrawImage road,i*StreetWidth+ScrollX, 300
Next
DrawImage car, 400,CarY
CarMove
Flip
Until AppTerminate()
Function CarMove()
If Rand(0,9)=0
Add = Rand(-1,1)
EndIf
CarY=CarY+Add/10
If CarY>265 Then CarY=265
If CarY<230 Then CarY=230
End Function
It looks like the car is driving, but it stand still at X=400. We "shake" the car a little bit by moving it seldom up and down.
Challenge I: Calculate
Write an app that calculates the sum of all numbers from 1 to 1000 with a For/Next
Challenge I: Jesu's Saving Book
If the parents of Jesus would have invested 1Cent (=0.01$) at his birth. And the bank would have paid only 1% interest a year ... How many dollars would now be on his saving book? Write an app that calculates anual interests and loops through 2121 years with For/Next .
Challenge III: Draw a chessboard
Write an app, that draws a chessboard by using nested For/Next loops. A chess has 8x8 fields with two different colors: dark wood and pale wood.
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/
Lesson XIII: Arrays... 100 Variables In One Go
In the last lesson we learned about a loop with 100 turns. Now we add a possibility to call hundred different variables within one loop with one code line.
Lets say we want to move a lot of identical things around: 10 cars in a racing game or 100 enemies in a game or 1000 snowflakes falling down or 9999 stars in the outer space. Each should move individual. How ca we do this?
With 10 cars you still would have the chance to code like that:
Global carX1:Int, carX2:Int, carX3:Int, carX4:Int, carX5:Int, carX6:Int, ....
Global carY1:Int, carY2:Int, carY3:Int, carY4:Int, carY5:Int, carY6:Int, ....
carX1=252
carX2=524
carX3=523
carX4=274
....
If CarX1 <0 then CarX1=0
If CarX2 <0 then CarX2=0
....
DrawImage CarImage1, CarX1, CarY1
DrawImage CarImage2, CarX2, CarY2
....
But that will be a lot of code lines! And what, if you need 100 or 1000? Or 10000?
Therefore we have the feature Array which offers us a bundle of variables all with the same name and a individual number (called "Index")
Global carX:Int[100], carY:Int[100]
carX[1]=253
carX[2]=524
...
Global Array:Int[100] defines 100 variables with index in square brackets. The first variable is adressed Array[0], the last with Array[99]
You can adress them individual as before with their individual names ( a combination of the name and a index in the bracket):
carX[1]=253
But now we also can call all 10 together with a For/Next-loop:
For local i:Int=1 to 10
If carX[i]<0 then carX[i]=0
Next
....
For local i:Int=1 to 10
DrawImage carImage[i], carX[i], carY[i]
Next
The variable "i" takes the job of being the index in the variable names. So we call carX[1], then carX[2] ... until carX[10]...
Balls jumping aroud
So lets do a first example:
We define 10 balls with 10 individual positions and speeds
Global ballY:Double[10], ballSpeed:Double[10]
For Local i:Int=0 To 9
ballY[i] = Rnd(100,500)
ballSpeed[i] =Rnd(0.5, 5)
Next
Now we can move them within the main loop:
...
Repeat
Cls
For Local i:Int=0 To 9
ballY[i] = ballY[i] + ballSpeed[i]
If ballY[i]> 580
ballSpeed[i] = -ballSpeed[i]
EndIf
Next
...
We also check if they reached the bottom of the screen. In this case we would turn around the direction but inverting the speed
Here is a complete code
I added a nice feature "Gravitation" by manipulating the variables ballSpeed[...] each turn.
SuperStrict
Graphics 800,600
Global ballY:Double[10], ballSpeed:Double[10]
For Local i:Int=0 To 9
ballY[i] = Rnd(-100,300)
ballSpeed[i] =Rnd(0.5, 5)
Next
Repeat
Cls
For Local i:Int=0 To 9
ballY[i] = ballY[i] + ballSpeed[i]
ballSpeed[i] = ballSpeed[i]*0.998 + 0.1 ' <------ feature: Gravitation
If ballY[i]> 580
ballSpeed[i] = -ballSpeed[i]
EndIf
Next
For Local i:Int=0 To 9
DrawOval 100+i*60, ballY[i], 21,21
Next
Flip
Until AppTerminate()
Challenge I: Snow Falls
Try to code a snowfall, where 2000 snowflakes starting at random positions above the screen, then falling down. When they reach the bottom, the start again above the screen.
Challenge II: 10 Crazy Cars
Try to code a demo, where 10 individual cars driving in all direction. When they left the screen they turn around with a random angle to come back.
Challenge III: Sort Numbers
Try to code a sorting algorithm: An array gets filled with 100 random numbers. Now try to find the highest number and copy it to the first index of a second (empty) array, the second highest number to the second index... and so on. Hint: mark the already copied values (in the first array) by setting them to "-1" so that they are to low to be taken in acount again.
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/
Lesson XIV: 2-dimensional Arrays are like board games
If you have a look on chess or checkers you can see a 2-dimensional field like a quad paper. This is also the base for a lot of computer games. Before we code a chess we want to have a look on maze (labyrinth) game like pacman or like bolder dash. Also 3D games like dungeon master are maze-based and not real 3D
Let us think of a gameboard of 12x12 fields:
SuperStrict
Graphics 800,650
Global Size:Int=50
SetOrigin 25,25
Repeat
Cls
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
DrawRect x*size , y*size, size-2, size-2
Next
Next
Flip
Until AppTerminate()
SetOrigin x,y moves the Drawing origin to the pixel position x,y. From now on all drawings are painted 25 more left and more down.
To fill a gameboard like this you need 12x12 = 144 variables. We can store them in an array of 144 elements. But the array can be organize already in 12 row with 12 elements
Global Board:Int[12,12]
Global Array:Int[n,m] defines a array of n*n elements organized in m rows, each with n elements. Caution: The indices of arrays always start with "0" and end at "n-1" or "m-1"
The left top corner variable is Board[0,0] the right neighbors are called Board[0,1] ...Board[0,11].
The second row starts with Board[1,0] and goes upto Board[1,11]
This continues until row 12:
The 12th row starts left side with Board[11,0] and goes upto Board[11,11]
Inside the variables we store, what is on this field. At the beginning all fields are 0. Means: "nothing in it"
Now lets define a "wall". "1" now stands for "wall". If you want to set a wall on position Board[0,0] you write
Board[0,0]=1
or if you want to fill a complete row with walls:
For Local I:Int=0 To 11
Board[i,0] = 1
Next
We build walls on all 4 bounds of the game bord:
SuperStrict
Global Board:Int[12,12]
Graphics 800,650
'top row & bottom row:
For Local I:Int=0 To 11
Board[i,0] = 1
Board[i,11] = 1
Next
'left column & right column:
For Local I:Int=0 To 11
Board[0,i] = 1
Board[11,i] = 1
Next
Global Size:Int=50
Repeat
Cls
SetOrigin 25,25
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
SetColor 255,255,255
DrawRect x*size , y*size, size-2, size-2
If Board[x,y]=1
SetColor 105,80,65
DrawRect x*size , y*size, size-2, size-2
EndIf
Next
Next
Flip
Until AppTerminate()
Now let us define a "player". The "2" stands for player. We put him anywhere, but not in a wall
...
Graphics 800,650
Global Player:TImage=LoadImage("player.png") ' <----- NEW LINE
...
Global Size:Int=50
Board[ 1+Rand(9) , 1+Rand(9) ] =2 ' <----- NEW LINE
Repeat
...
If Board[x,y]=1
SetColor 105,80,65
DrawRect x*size , y*size, size-2, size-2
ElseIf board[x,y]=2 ' <----- NEW LINE
SetColor 255,255,255 ' <----- NEW LINE
DrawImage player, x*size , y*size ' <----- NEW LINE
EndIf
...
You find the little collegue player.png in the attachment.
The For/Next loops scans all 144 elements of the array and paints something related to the content of the element. If the array variable at (X,Y) has the content 2 it draws the image of the player at (x*50,y*50)
Now lets move the player
We us the keyboard cursor keys to move the player 1 step to the left (:
Function CheckPlayer()
If KeyHit(KEY_LEFT)
FindAndMovePlayer(-1)
Else
....
EndIf
End Function
Function FindAndMovePlayer(moveX:Int)
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
If board[x,y]=2
Print "found at " + x+ " " + y
board[x+moveX ,y]=2
board[x,y]=0
Return
EndIf
Next
Next
End Function
First we search for the "player's" element, then mark the field left of it with a "2". Do not forget to mark the prior field with "0" because now it is empty. If you forget it you suddenly will have two players . Then leave the function immediately.
Now we need to do this four all 4 directions:
SuperStrict
Global Board:Int[12,12]
Graphics 800,650
Global Player:TImage=LoadImage("player.png")
'top row & bottom row:
For Local I:Int=0 To 11
Board[i,0] = 1
Board[i,11] = 1
Next
'left column & right column:
For Local I:Int=0 To 11
Board[0,i] = 1
Board[11,i] = 1
Next
Global Size:Int=50
Board[ 1+Rand(9) , 1+Rand(9) ] =2
Repeat
Cls
SetOrigin 25,25
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
SetColor 255,255,255
DrawRect x*size , y*size, size-2, size-2
If Board[x,y]=1
SetColor 105,80,65
DrawRect x*size , y*size, size-2, size-2
ElseIf board[x,y]=2
SetColor 255,255,255
DrawImage player, x*size , y*size
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)
Print "move"
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
If board[x,y]=2
Print "found at " + x+ " " + y
board[x+moveX ,y+moveY]=2
board[x,y]=0
Return
EndIf
Next
Next
End Function
to prevent that the player move on a wall-field, we should check, that the new target field is empty, before we moe the player:
Function FindAndMovePlayer(moveX:Int, moveY:Int)
Print "move"
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
If board[x,y]=2
Print "found at " + x+ " " + y
If board[x+moveX ,y+moveY]<>0 Then Return ' <----- NEW LINE
board[x+moveX ,y+moveY]=2
board[x,y]=0
Return
EndIf
Next
Next
End Function
When the player "finds" the gold (="3") a hole (="4") in the wall enables to exit end enter the next level
Final Version
SuperStrict
Global Board:Int[12,12]
Graphics 800,650
SetBlend alphablend
Global Player:TImage=LoadImage("player.png")
'top row & bottom row:
For Local I:Int=0 To 11
Board[i,0] = 1
Board[i,11] = 1
Next
'left column & right column:
For Local I:Int=0 To 11
Board[0,i] = 1
Board[11,i] = 1
Next
'some additional walls:
For Local I:Int=0 To 7
Board[4,i+3] = 1
Board[i+2,6] = 1
Next
For Local I:Int=0 To 3
Board[7,i+1] = 1
Board[i+2,2] = 1
Next
Global Size:Int=50
Board[2,4] =2
Board[6,8] =3
SetClsColor 125,125,100
Repeat
Cls
SetOrigin 25,25
' gray floor:
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
SetColor 155,155,155
DrawRect x*size , y*size, size-2, size-2
SetColor 1,1,1
DrawRect x*size+2 , y*size+2, size-2, size-2
SetColor 111,111,111
DrawRect x*size+2 , y*size+2, size-4, size-4
Next
Next
'shadows:
SetColor 1,1,1
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
If Board[x,y]=1
SetAlpha 0.05
For Local i:Int=0 To 6
DrawRect x*size+i*3 , y*size+i*2, size, size
Next
ElseIf board[x,y]=2
SetAlpha 0.1
DrawOval x*size+18 , y*size+38,30,15
EndIf
Next
Next
SetAlpha 1
'all other elements:
For Local y:Int= 0 To 11
For Local x:Int= 0 To 11
If Board[x,y]=1
SetColor 145,120,95
DrawRect x*size , y*size, size-2, size-2
SetColor 105,80,65
DrawRect x*size+2 , y*size+2, size-4, size-4
ElseIf board[x,y]=2
SetColor 255,255,255
DrawImage player, x*size , y*size
ElseIf board[x,y]=3
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= 0 To 11
For Local x:Int= 0 To 11
If board[x,y]=2
If board[x+moveX ,y+moveY]=3
board[0,8]=4 ' "4" is exit-field
ElseIf board[x+moveX ,y+moveY]=4
End
Else If board[x+moveX ,y+moveY]<>0
Return
EndIf
board[x+moveX ,y+moveY]=2
board[x,y]=0
Return
EndIf
Next
Next
End Function
Challenge I: Start A Chess Game
Code a chess board, where only the pawns exist. Try to move them "ahead" by random every 2 seconds.
Challenge II: Connect Four
Code a complete Connect Four game (Captain's mistress). Two players throw alternately a chip into the maze. The chips fall down to the deepest possible position. The game is over if a player has 4 chips of his color in one row, column or also diagonal.
(https://www.syntaxbomb.com/tutorials/learning-basic-for-total-beginners-blitzmax-ng/?action=dlattach;attach=5210)
Challenge III: Expand our maze
Try to add new feature to the maze like "doors" and "keys" inside the room. Try to create a complete new room.
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/
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.
(https://www.syntaxbomb.com/tutorials/learning-basic-for-total-beginners-blitzmax-ng/?action=dlattach;attach=5174)
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:
(https://www.syntaxbomb.com/tutorials/learning-basic-for-total-beginners-blitzmax-ng/?action=dlattach;attach=5176)
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:
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
Variable = Variable | element
BIT-wise AND checks whether this element is in the variable
If (Variable & element)=element
BIT-wise AND XOR removes this element from the the variable
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:
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:
' 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:
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:
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:
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/
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:
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...
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:
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:
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:
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:
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
...
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:
...
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:
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
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:
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/
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:
Const DOOR:Int = 16
Const KEY:Int = 32
Change 2: Define a related Player's variable:
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:
Add 4,8 , KEY
Add 11,4 , DOOR
Change 4: Display the elements
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:
DrawText "YOU HAVE " + P_Keys + " KEY ", 200,5
Change 6: Define what happens when the player "finds" the elements:
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
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:
SuperStrict
Graphics 800,650
' SeedRnd 34567
Print "This Seed=" + RndSeed()
....
Users version:
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/
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:
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:
Add 1,1 , ENEMY
Add 1,10 , ENEMY
Add 10,10 , ENEMY
Change 4: Display the elements
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:
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
...
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.
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:
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:
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():
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:
'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.
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/
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?
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:
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.
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:
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:
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/
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!!!!
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.
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?
Lesson XX: Text-Files and String ManipualtionToday 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-FileTxt-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:
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:
... = 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:
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"
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:
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
=...
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
...
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 STRINGSuperStrict
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 DOUBLEGlobal 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
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(...).ToIntHow 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
....
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 ARRAYFunctions can return arrays:
Function SplitToArray:Int[] (...)
Local Integer:Int[...]
....
Return Integer
End Function
We cannot only return single variables, but also complete arrays
NEST FUNCTIONSWe can nest function calls into function calls:
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/
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:
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:
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.
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:
SuperStrict
Global MyIni:String[]
IniLoad("ini.txt")
...
towards...
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:
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()
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:
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:
Global PlayerName:String = IniRead("Name")
Global PlayerAge:Int = IniRead("Age").ToInt
Global PlayerHealth:Double = IniRead("Health").ToDouble
Global PlayerGadget:Int[] = SplitToArray(IniRead("GameGadgets"))
...
towards...
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()
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
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/
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():
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:
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.
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:
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:
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:
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()
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...
Print "IniWrite at " + linenumber + " ->" + Ini[LineNumber]
... verifies that we truely entered the function. The second...
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/
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:
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:
...
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:
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:
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:
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/
Lesson XXIV: A Type Is A Class? (OOP Step 2)(https://www.syntaxbomb.com/tutorials/learning-basic-for-total-beginners-blitzmax-ng/?action=dlattach;attach=5282)
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 TypeWe 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:
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.
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.NameIf you want to work with a
library variable write a compound word
ClassName + Dot + VariableName Change the Class Name in your main appIf you do not like the given library name you can also exchange it. We call this trick: Create an instance of the Type:
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 WatchThis 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
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:
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 GadgetNow you can code whatever you want. The TTimer is a closed world like an app in the app:
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
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
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/
Radio Group Buttons
I expanded the last lesson to demonstrate RADIO BUTTONS. Please have again a look on the previous lesson:
https://www.syntaxbomb.com/index.php?msg=347060951
The Button Neighbour Problem
If more than one button can be pressed at the same time, you need different images for the case, when two neighbours are pressed:
RadioButtonProblem.gif
This need 2 more images and a complex search algorithm:
Method FindBest:Int()
If Selected=0 Return 0
If Search(ID-1)
If Search(ID-1).Selected=True
If Search(ID+1).Selected=True
Return 4
EndIf
Return 2
EndIf
EndIf
If Search(ID+1)
If Search(ID+1).Selected=True
Return 1
EndIf
EndIf
Return 3
End Method
Method Search:TButton(SearchId:Int)
For Local loc:TButton=EachIn All
If loc.ID=SearchID
Return loc
EndIf
Next
Return Null
End Method
Now it looks better:
RadioButtonSolved.gif
This is the whole code:
SuperStrict
Graphics 500,200
For Local i:Int= 0 To 5
New TButton(i+1,30+i*66,30,66,122, 0)
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("VintageButtonBetter.png",66,122,0,5)
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
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
If Search(ID-1)
If Search(ID-1).Selected=True
If Search(ID+1).Selected=True
Return 4
EndIf
Return 2
EndIf
EndIf
If Search(ID+1)
If Search(ID+1).Selected=True
Return 1
EndIf
EndIf
Return 3
End Method
Method Search:TButton(SearchId:Int)
For Local loc:TButton=EachIn All
If loc.ID=SearchID
Return loc
EndIf
Next
Return Null
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
And you need this better PNG to run the app:
VintageButtonBetter.png