Mohaa Scripting Advice
Mohaa Scripting Advice
PLEASE NOTE
This might help you to do good or better MOHAA scripting, but it is not a scripting tutorial.
FOREWORD
With my recent endeavour into MOHAA scripting, I was quite surprised. I have seen many creative ways to
do scripts in the past years, but I have never imagined such a messy code, I have witnessed in MOHAA.
The global, as well as the level scripts are really terrible examples of scripting. With this Document I will try
to walk you trough my idea of a good script code in MOHAA. It will include many of the basics you
probably are aware of, but non the less, you should take a moment to see if I can inspire you just a little.
THANK YOU
This tutorial got improved with the input of others, that shared their opinion and advice with me.
Thanks to x-null forums users: 1337Smithy, DoubleKill, James, Purple Elephant, RyBack
Others that provided input and helped: Criminal, Daggolin, NeMs, Todesengel
INDEX
VARIABLES
CVARS
ENTITIES
MAIN
BRACKETS
USING HEADER AND QUOTES
INDENTING
TYPE CASTING
NULL AND NIL
NO LOCAL STRINGS
USE THE SDK DOCUMENTATION
ASK AND LEARN FROM OTHERS
DO NOT USE GOTO
ENTITY FLAGS
EACH FRAME
USE VARIABLES OVER EVENT CALLS
NAMEING
WAIT FOR PLAYER
WAIT FOR WARMUP TIME
LOOPS
SWITCHES
SELF
PARM
EXTERNAL SCRIPT THREADS
RETURN VALUES
END
VARIABLES ( local. )
There are different kind of variables in mohaa, which are working in different Zones. I will try to give you a
few examples what that means.
CVARS ( getCvar("sv_gravity") )
Cvars are Configuration-variables, they can be permanently stored in the configuration file of the
server/player. They remain in the configuration file until they are removed by a user or a specific console
command. Shutting down the server will not remove a cvar from the configuration file.
In the code below you can see examples how to get and set cvar values by script.
1 local.gravity = int( getCvar("sv_gravity") ) //get cvar from server cfg
2 setCvar("sv_gravity" "30") //set cvar to server.cfg
You should only use Cvars if you want to preserve data for the next server start. There are limits, Cvars have
a length limit and the configuration file has a maximum filesize limit right below 8 Kilobyte. You are
adviced to store as little information as you can. Here is a example:
Short and good To long and wasteful
setCvar("coop_plH1" "67") setCvar("cooperative_playerOne_HealthOnLastLevel" "67.000")
ENTITIES ( $world )
Entities are level Objects such as players, actors, items, script_objects, health-packs, projectiles, decals,
grenades...
Everything that can not be changed in game, like the actual geometry of the level and all static models, are
grouped together to one big $world entity. The world entity gives control over weather, fog, physics and a
few more other neat things on the level.
Entities are accessible via script by their targetname. You can easily spot them by the $ in front of their
targetname.
MAIN ( main: )
Every level script and some of the global scripts should contain a function with the name main. This main
thread is started by the game as soon as the level is loaded and all entities are spawned (with the exception
of clients / players) .
The main thread is mostly used to initialize setup threads. These setup threads are used to prepare the
scripted sequences, Actors, exploding Objects and other entities on the level. Additional script threads can be
started from Script, Triggers or Actors.
The main thread should be kept short and light weighted, so if you have a intense script to run you should
start a separate thread.
The example code below shows how a main thread is presented in the level scripts. The dots ("...") are
symbolic for any script code placed there.
1 main:
2 ...
3 end
NOTE: The main thread in the primary level script is started by the game without any parameters. More
about parameters will come later...
BRACKETS ( { } )
The biggest issue that MOHAA script code has, is the poor use of brackets. Brackets make it visually clear
where a code block starts/ends. I recommend useing brackets where ever you can. I use Notepad++ that can
collapse and expand code if brackets are used.
Using brackets will make your work much easier, especially on large portions of code. I will give you a few
examples, how to make good use of the brackets.
1 main:{ On the left is a example how the code looks when it is expanded, on the 1 main:{
2 ... right how it looks like when it is collapsed in Notepad++. 4
3 }end
If you collapse a few threads you get a pretty nice overview of your script, 5
allowing you to navigate easier in the code.
But this is not all brackets can do, they are not just there to make it collapsable. They also make it clear how
the code is suppose to work. Most errors I have seen in MOHAA code are caused, because no brackets are
used. Let me give you a example.
1. without brackets
1 doSomething:{
2 if( $someAi cansee $player )
3 $someAi attackplayer
4 iprintlnbold_noloc ( "can see" )
5 }end
2. with brackets
1 doSomething:{
2 if( $someAi cansee $player ){
3 $someAi attackplayer
4 }
5 iprintlnbold_noloc ( "can see and attack" )
6 }end
The code on the first table above seams to be different from the second, but I guarantee you they are exactly
the same! Without brackets only the first code line directly below the if statement is effected by the statment.
The next line of code with iprintnbold_noloc will always be executed regardles of the result from the if
statment.
I will give you another example what consequences this can have.
1. without brackets
1 local.player = NULL
2 for( local.i = 1; local.i <= $player.size; local.i++ )
3 local.player = $player.[local.i]
4 if( local.player != NULL )
5 iprintlnbold_noloc ( "player exists" )
2. with brackets
1 local.player = NULL
2 for( local.i = 1; local.i <= $player.size; local.i++ ){
3 local.player = $player.[local.i]
4 }
5 if( local.player != NULL ){
6 iprintlnbold_noloc ( "player exists" )
7 }
The code without brackets, suggests that it checks all players, but it checks only the last player, upon exiting
the for-loop, exactly like the code with the brackets does.
So here is the catch: If this script is tested by the developer it works just fine, the error in this script will only
show, if there is more than one player on the server. The Code below shows how tit should have been done.
1 local.player = NULL
2 for( local.i = 1; local.i <= $player.size; local.i++ ){
3 local.player = $player.[local.i]
4 if( local.player != NULL ){
5 iprintlnbold_noloc ( "player exists" )
6 }
7 }
Brackets allow you also to applay special formatting, which can be better in some cases. If you need a
codeblock in one Line, like in the example below.
1 if( local.water = NIL ) { local.water = "wet" }
2 if( local.rock = NIL ) { local.rock = "hard" }
3 if( local.fire = NIL ) { local.fire = "hot" }
INDENTING ( ->| )
In this regard the MOHAA script code is examploary, showing how NOT to do it! Indenting code correctly
can improve the readability of your script, but it can also do the opposite if it is done incorrectly.
The example code on the left and right is identical in its function, but you will need much more time for
reading and understanding the left code .
1 if( local.var == 1 ) 1 if( local.var == 1 ){
2 iprintlnbold_noloc "You die." 2 iprintlnbold_noloc "You die."
3 else 3 }
4 if( local.var == 2 ){ 4 else if( local.var == 2 ){
5 iprintlnbold_noloc "You live." 5 iprintlnbold_noloc "You live."
6 else 6 }
7 iprintlnbold_noloc "Try again." 7 else{
8 8 iprintlnbold_noloc "Try again."
9 9 }
I have seen lots of code in the global script files like this,
and I really can not express how bad this is, without swearing
for at least 5 minutes.
My advice to you is not to write code like you are stuck between the
80's and early 90's with a Monochrome 480 x 320 Pixel monitor.
NOTE: Clean, easy to read code can reduce or even prevent errors.
...getcvar will retun the data of the Cvar as a string. You need to convert the data if you want it as a integer.
Entities that exist are never NULL, so checking against NULL can tell us if a specific entity exists or not.
1 if( $friendly1 != NULL){ … } //if entity exist
Checking agianst NIL allows you to find out if a variable is empty or set.
1 if( local.isRaining != NIL){ … } //if variable has a value
Sometimes you have to check for both, NIL and NULL, if you store entities inside variables. Check them in
the right order, as shown in the code below, to get the desired result.
1 if( local.player != NIL && local.player != NULL){ … } //has value & entity exists
NO LOCAL STRINGS ( iprintlnbold_noloc("") )
If you show text to the players, that is not part of the game its default local strings, you should make use of
the command iprintlnbold_noloc. This will prevent the game from printing a error message to the console
and creating a error log of missing local strings.
Make sure you take notes of commands that might be useful to you and test how they work. This will make
the scripting much easier for you in the long run.
The problem is how entity variables are visually displayed in the code. First the example how it looks
without flags, then how it looks with flags. Using flags will make it obvious that it is a entity variable, not a
regular script command applied to a entity.
1 $captainWonders.health = 10 //set enity variable
2 $captainWonders health 10 //set health and max_health
3 local.health = $campatinWonders.health //get health value
More experienced scripters might not have as much trouble with it as beginners will, but if you can avoid
any possible source of errors, you really should.
EACH FRAME ( waitframe )
Servers do not just run your script once per second, they do it a couple of times per second. How often
depends on the cvar sv_fps. On default it is set to 20, that makes 20 frames per second. Or in other words,
one server frame time is 0.05 seconds.
Not every server has the same settings, but for some commands to work right you need to wait one frame in-
between. Luckily MOHAA has a own script command to help you there, it is called waitframe. It pauses the
script (just the current thread/code, not all script) at the position it is placed, and continues as soon as the
current frame time has ended and a new frame time has begun.
You will see waitframe often used inside of while and for loops, making sure the code is run only once per
frame time. That makes perfect sense, because all entities on the server are also updated once per frame
time, so checking for changes, needs to be done only once per frame.
You can store Cvar values that do not change, in script variables. These are controlled by the script, so they
do compute much faster than using the getcvar command. But be warned, this only makes sense if you know
that this cvar does not change while you are using it.
Grab Cvar values that don‘t change in your map script right above the main thread, like shown in the
example. But be careful if you specify variables outside of a function they are set each time you exec the
script file (exec maps/myscript.scr), unless you specify a thread (exec maps/myscript.scr::main).
1 local.g_gametype = int( getCvar( "g_gametype" ) )
2 main:{
3 if( local.g_gametype > 0){
4 wait 10
5 iprintlnbold_noloc("this is multiplayer")
6 }
7 }end
NAMEING
Naming of variables and functions is very important! Especially if you plan to work on bigger projects.
Good naming has a positive impact on the code quality. I have seen alot of fancy naming, but good naming
is not about making it look fancy, it is about easy to understand and fast to read code.
1. Function names start in lowercase and words are seperated either by a uppercase letter or an
underscore.
Examples: actorAnimateNow, actor_animate_now or actor_animateNow
2. Variable Names should also follow the naming of functions, however, you may want to add the type
of the variable to the variable name.
Examples: local.string_missionText, level.integer_missionStatus
4. Make sure you have clearifying commentaries where they are needed.
If it is not totally obvious what a function or variable does, make sure your comments make it clear.
Not everyone would chose the same name, so not everyone will know what you intend to express.
WAIT FOR PLAYER (level waittill spawn )
Once the map is fully loaded on the server, the scripts are executed. At this moment Players just start to load
the map on their computer, and it will take them a moment before they can enter the game. During this time
the scripts are running without a player present. (This can also happen in singleplayer)
I have been told that there is a command to wait for players in multiplayer, but I have never tested it.
1 level waittill playerspawn
The example code below shows how it could be done in single- and multiplayer.
1 waitForPlayers:{
2 if( int( getCvar( "g_gametype" ) ) ){ //Singleplayer
3 level waittill spawn //wait until player spawn
4 local.player = $player[1]
5 local.player healthonly 111
6 }
7 else{ //Multiplayer
8 while( $player == NULL ){ //loop until a player joins
9 waitframe
10 }
11 for( local.i = 1; local.i <= $player.size; local.i++ ){ //handle all players
12 local.player = $player[local.i] //get a player
13 if( local.player != NULL ){ //if player exists
14 local.player healthonly 111
15 }
16 }
17 }
18 }end
The code below is a good exmple how to pause your script/function until the warmup time is over.
1 while( int(getCvar("g_warmup")) >= level.time){
2 waitframe
3 }
A infinity loop error is assumed if a loop has been iterated (cycled) for a coupe of thousend times, within a
single server frame (0,05 sec at sv_fps 20).
If you need your loop to run for a longer 1 while( 1 ){ //run infinite
duration of time, you should use a 2 //exit loop if player gone or dead
3 if( $player == NULL || $player.health <= 0){
command that will pause your loop for a 4 break
short time. 5 }
6 $player heal 1 //heal player each frame
You can use wait or waitframe. 7 waitframe
8 }
waitframe will pause your loop temporarily and wait until the beginning of the next server frame time,
before resuming. This will run your loop once per frame and prevent your loop from being errored out.
wait followed by a float or integer (wait 1.5), will pause your loop temporarily and wait until the time in
seconds has passed, before resuming.
Loops have two special commands, that can be very useful!
Continue will make a loop skip forward to the next Iteration/Cycle.
Break will break out of the loop, this will end the current loop.
The while loop is interated once per 1 //loop as long as the entity does exist
server frame. 2 while( $enemy != NULL){
3 //end the for loop if enemy is dead
4 if( isAlive $enemy != 1){ break }
The while loop will 5
break (end instantly) if the entity 6 //handle each player in mp
$enemy does no longer exist. 7 for( local.i=1;local.i<=$player.size;local.i++){
8 //grab player from array
The for loop will interate as often as 9 local.player = $player[local.i]
10 //if player does not exists, go to next cycle
there are players on the server. 11 if( local.player == NULL ){
12 continue
The for loop will skip forward to the 13 }
next iteration and ignore the rest of 14 //will only execute if player != null
the code in the current iteration if the 15 local.player heal 1
16 }
current player does not exist. 17
18 //end while loop if enemy dead enemy
19 if( isAlive $enemy != 1 ){ break }
20
21 //heal enemy
22 $enemy heal 1
23
24 //wait for this frametime to end
25 waitframe
26 }
Example of break and alternative means. The two examples are equal in their function, but sometimes it
is better to use a break and sometimes it is not to. This depends on the structure of your code and if there are
multiple conditions you want to check in a specific order or just right at the start or end of the loop.
Example of continue and alternative means. Using continue can prevent your code inside a loop from
becoming to much interleaved. As you can see on the left example code with all these if statements. If you
notice that your code is getting to interleaved and to hard to read, you should consider making use of break
and continue instead, like on the example code on the right.
1 for(local.i=1; local.i<=$player.size; local.i++){ 1 for(local.i=1; local.i<=$player.size; local.i++){
2 local.player = $player[local.i] 2 local.player = $player[local.i]
3 //if player exists and is alive 3 //continue with next cycle if player missing or dead
4 if( local.player != NULL && local.player.health > 0){ 4 if( local.player == NULL && local.player.health <= 0){
5 if(isAlive $enemy){ 5 continue
6 if( isAlive $enemy2 ){ 6 }
7 if( isAlive $enemy3 ){ 7 //continue with next cycle, if player health > 10
8 if( local.player.health < 11 ){ 8 if( local.player.health >= 10 ){
9 local.player heal 0.5 9 continue
10 } 10 }
11 } 11 //abort if any enemy is dead
12 } 12 if( !isAlive $enemy1 ){ break }
13 } 13 if( !isAlive $enemy2 ){ break }
14 } 14 if( !isAlive $enemy3 ){ break }
15 } 15 //heal current player upto 50%
16 16 local.player heal 0.5
17 17 }
SWITCH
On the example code above you can see a classical example of a switch construct. The switch converts the
value of the expression (in this case the variable local.var) into a string for comparison.
SELF
In MOHAA scripting you will often see the use of the object self.
Self is the object that started the current 1 //example function using self
function. 2 main:{
3 //stop if self does not exist
4 if( self == NIL || self == NULL ){ end }
This means self can be a different object 5 self health 1000
each time it is being used. To figure out 6 self scale 2
what self is you need to know where or 7 }end
what started this function.
Triggers, Entities, and scripts (AI/Global) can start functions in the script, and what ever started the function
will be accsessible as self in the function.
Actors can run threads and also become self, they can then perform advanced script actions.
If a function is started by a trigger this trigger will be self in that function it did start.
MOHAA allowes you also to get the entity that activated (entered/used) the trigger, with another object
reference (parm.other), more about this in the next chapter.
PARM. (parm.other)
There are several parm Reference Objects, each has its own purpose and can only be used under certain
circumstances. Each parm Reference Objects stores only one Object at a time, which is globaly accsessible.
This means that the Object can be overwritten by the game at any given moment.
To keep the object, you need to put it immediately into a local variable!
1 local.other = parm.other
parm.other
Can be used if a function is started by a trigger. What ever activated the trigger which started the function
shown in the code below will be parm.other.
1 //example function using parm.other, needs to be started by a trigger
2 showTargetnameOther:{
3 local.other = parm.other
4 if( local.other == NIL || local.other == NULL ){ end } //exit if invalid/missing
5 iprintlnbold_noloc( "parm.other has the targetname:"+local.other.targetname )
6 }
parm.owner
Is similar to parm.other, but it is used to get the owner of the projectile that has activated the trigger which
started the function.
1 showTargetnameOwner:{
2 local.other = parm.other //get activating entity
3 local.owner = parm.owner //get owner of activating entity
4 if( local.owner == NIL || local.owner == NULL ){ //fallback to other if no owner
5 if( local.other == NIL || local.other == NULL ){ end } //exit if missing
6 local.owner = local.other //fallback
7 }
8 iprintlnbold_noloc( "parm.owner targetname:"+local.owner.targetname )
9 }
parm.previousthread
Returns the thread as a object that was previousely started, it is used to have control of a thread outside of
the actual thread. The example code below is from one of the global game scripts.
1 //example how to use parm.previousethread
2 friendlythink:{
3 //start a thread
4 thread friendlythinkstart
5 //retrive the started thread
6 local.thread = parm.previousthread
7 //wait until actor dies
8 self waittill death
9 //check if thread is still running, delete if it is
10 if (local.thread){
11 local.thread delete
12 }
13 }end
You will often see exec being used in mohaa to call functions in external script files. There are two ways you
can execute a file with exec. With a function name attached, like shown on the left in the table below, or
without a function name.
NOTE: There is a huge difference between the two ways of doing this!
Executing only a function Executing the complete file
exec myFolder/myFile.scr::myFunction thread myFolder/myFile.scr
This starts only one function, in this case the This executes the script from top to bottom.
function: - local.var1
- myFunction. - level.var1
- main:
RETURN VALUES
Functions can return values, this is especially interesting if they are used for checking. In the code below the
entity in the local.entity variable is checked against all valid players on the server. If any valid player is
touching the entity the function will return 1, otherwise it will retun 0.
1 if(exec coop_mod/replace.scr::istouching local.entity){
2 iprintlnbold_noloc("A player is touching: "+local.entity)
3 wait 5
4 }
Using a external function for this, rather than typing all the code in this file, reduces greatly the ammount of
code and it keeps the code comprehensible. This is why HaZardModding Coop Mod uses this techniqe.
You can use exec, waitexec and waitthread for this, if you are using exec be aware that you can not use any
kind of wait command (waitframe/wait) in the external function, use waitexec or waitthread then.
The example code below shows how a function returning a value could look like.
1 returnValue local.var1 local.var2:{
2 local.result = 0
3 if(local.var1 == local.var2){ local.result = 1 }
4 }end local.result
END
Your code should be beautiful, practical but above all functional.
If you have to choose between beauty and practical, go with practical.
But make sure it is functional at all times, by conducting thoroughl tests.
Thank you for reading, I hope I could give you some useful Advice.
If you have any feedback, feel free to contact me.