Tutorial Details:
Difficulty Level:
Intermediate
Topics Covered: Re-creating
the Energy Crisis mission using MIndscript.
Assumed Knowledge:
Most Basic and Intermediate topics.
Written By: BILL LANE
BACK
The aim of this tutorial is to look at the steps involved
in creating a basic Spybot mission by re-creating Energy Crisis.
It may seem that I'm re-inventing the wheel by re-creating
an existing mission. But I have often used the process of
imitation as a good starting place to learn a new skill. For
a start I have a model I can easily refer to that will tell
me how I'm going. The other thing is I know what I'm attempting
is possible.
So where do we start? I started by making a list of the things
that need to happen. My list looked like this:
- Calibrate light
- Detect bump to start
- Detect bump if obstacle hit
- React to obstacle hit
- Detect light when mission achieved
- Perform the victory dance
- Detect low time
- Respond to low time
- Detect time up
- Respond to time up
- Random shocks
Note that I've seperated events and their respective responses.
Each of the events will be monitored by a watcher. But I'm
not going to let the watcher respond itself. Instead I'm going
to maintain a variable that keeps track of whats happening
in the mission and let the watchers make changes to that variable.
That way there will be less chance that our outputs will receive
conflicting instructions. So one of the first things I want
to do is to declare and set a global variable to keep track
of the current state of play. I'm going to need a total of
5 variables and 2 constants. So let's declare them now and
I can explain them as required:
var gv_energyLevel = 6
var gv_state = 0
var gv_shockTime
var gv_lastShocktime = 0
var gv_timeSinceShock = 0
const gc_nearTime = 500
const gc_GameTimeLimit = 600
I've started the mission with the state equal to zero. It
will remain in this state until the touch sensor is tapped
the first time. The other states are:
0 ready to start
1 running
2 hit object
3 shocked
4 timeUp/out of energy
5 mission complete
The other thing to note from my initial list is that I haven't
listed any controller events. In this mission we direct the
Spybot in RC mode so we won't need to write any code to handle
button presses.
Looking over my list I can see I'll need a few includes.
Spybot.h declares my motors and sensors so I'll definitely
need to include that. Globals.h declares all the light, sound
and movement subs and constants; I'll definitely want that.
Events.h declares the BumpEvent for the touch sensor and I'll
need that. So I'll start by including those three headers:
#include<Spybot.h>
#include<Globals.h>
#include<Events.h>
Returning to my list the first item is Calibrate light. The
Spybot will automatically calibrate the light sensor when
we press the RUN button. So I don't need to do anything there.
But I will want to set up an event and a watcher to detect
that the light has been reached. Here's the event declaration:
event lighthigh when opto.normal
This goes after the includes but before the main task. The
watcher goes after the main task and it looks like this:
watcher lightWatch monitor lighthigh{
if gv_state <> 0{gv_state = 5}
}
Note the watcher doesn't perform any actions. All it does
is set the game state to 5 (mission complete) if the event
occurs when the game state isn't 0 (we can't win if we haven't
started). The only other thing we need to do to make the light
sensor work is to start this watcher. This happens inside
the main task:
start lightWatch
Next on the list is detect bump to start. The BumpEvent has
already been declared in events.h so all we need to do is
to set up a watcher:
watcher hit monitor BumpEvent{
select gv_state{
when 0{ start initGame}
when 1{gv_state = 2}
}
}
Here we use a select statement because we want the watcher
to repond differently at the start of the game than it will
during the game. If the sensor is hit while the state is 0
then the watcher will start the initGame task(we'll have a
look at that in a moment). If the mission is running (gv_state
= 1) then it will reset the mission to state 2 (hit object).
When that's in place we will start this watcher in the main
Task.
Now let's go back and have a look at initGame:
task initGame{
LED[iYellowBlink] = 0
LED[iYellowWarn] = 0
sound 3
display 5
wait 100
clear sound
clear display
start BioTick
gv_shockTime = random 150
start randomShockCounter //starts the random shock mechanism
gv_state = 1
}
This is one of three user defined (non watcher) tasks that
I'm going to use. These tasks are declared afer the main task
and before the watchers. The first two line turn off the ALERT
light, play a sound and run the point to front LED animation.
It then waits 1 second to let that finish and clears the sound
and display. Next ti starts another task called BioTick. This
task runs a forever loop that increments a counter (nBioTick
declared in globals.h) 10 times every second. I'm going to
use nBioTick as a kind of timer to watch for the end of the
game. Here's the task:
task BioTick {
clear nBioTick
forever{
nBioTick += 1
wait 10
whatNext
}
}
This task also runs a sub-routine whatNext on every pass
through the loop. Sub whatNext uses a select statement to
check the value of gv_state and to respond as required. If
we can consider BioTick as the missions heart then whatNext
can be seen as it's brain. We'll take a closer look at whatNext
soon. But first lets consider the rest of initGame. initGame's
next command is to set another one of our global variables
gv_shockTime to a random number between 0 and 150. This variable
will tell the program when to give our Spybot a shock. This
mechanism is controlled by another task randomShockCounter
which is the next thing initGame starts.
N.B. When you look at the finished code you'll notice the
command randomize at the top of the main task. This command
resseds the random number generator. Using this command at
the start of a program will get you a more random result.
Before we look at the randomShockCounter just note that
initGame's final command is to set gv_state to 1 (the mission
is now running).
task randomShockCounter{
clear nStateWatcher
forever{
nStateWatcher += 1
wait 10
if nStateWatcher = gv_shockTime{gv_state = 3 stop randomShockCounter}
}
}
Like BioTick the randomShockCounter runs a forever loop.
This time incrementing another counter (nStateWatcher also
declared in globals.h). The difference is that when this counter
reaches the random value gv_shockTime this task resets the
state to 3(shocked) and then shuts itself down.
There are two other events and their watchers that we haven't
looked at. Both of these events relate to the nBioTick counter.
One is triggered when time is up and the other is triggered
when time is nearly up. Here are the event declarations:
event timeUp when nBioTick = gc_GameTimeLimit
event nearTime when nBioTick = gc_nearTime
Note the use of the two constants declared at the top of
the program to test when the events should trigger. Now here
are the watchers:
watcher timeUpWatch monitor timeUp{
if gv_state = 1{gv_state = 4}
}
watcher nearTimeWatch monitor nearTime{
if gv_state <> 0{timeShort}
}
The timeUp watcher resets gv_state to 4 (time up) and the
nearTime watcher starts a sub-routine called timeShort.
sub timeShort{
local flashInt = gc_GameTimeLimit - nBioTick
LED[iYellowBlink] = 1
forever{
LED[iYellowBlinkInterval] = flashInt
repeat 10{sound 1 wait flashInt}
flashInt = gc_GameTimeLimit - nBioTick
}
}
Sub timeShort declares a local variable flashInt to control
the frequency of a forever loop and the blink interval of
the alert light. This variable is set to the differnce between
the total time and the current time.The loop has a repeat
that plays a short sound 10 times after which the flashInt
variable is updated. The effect of this is that the sound
is played more frequently the longer the loop continues. Indicating
that timeUp is getting closer and closer.
You'll notice that there isn't much happening in the main
task. It starts the watchers, does a few things like randomize,
but that's all it does. Most of the action comes out of whatNext.
So let's have a look at that next:
sub whatNext{
select gv_state{
when 1{lightUp}
when 2{hitObject}
when 3{shocked}
when 4{loseGame}
when 5{winGame}
}
}
Remember that whatNext is called by BioTick 10 times a second.
When it's called it uses a select statement to check the value
of gv_state and to call a suitable sub-routine based on it's
value. When gv_state = 1( normal running) it calls a sub called
lightUp:
sub lightUp{
if gv_state <> 0{
select gv_energyLevel{
when 6{LED[iDisplay] = 63}
when 5 {LED[iDisplay] = 55}
when 4 {LED[iDisplay] = 39}
when 3 {LED[iDisplay] = 7}
when 2 {LED[iDisplay] = 3}
when 1 {LED[iDisplay] = 1}
when 0 {gv_state = 4}
}
}
}
Sub lightUp checks the value of gv_energyLevel and updates
the ARC lights based on how much energy remains. If no energy
remains it resets the state to 4 (timeUp/out of energy).
Back in whatNext if the state value had been 2 (hit object)
it calls sub hitObject:
sub hitObject{
try{
if gv_energylevel = 1{gv_state = 4 gv_energyLevel = 0}
else{
Set_Bead(SetRCDisable,1)
gv_energyLevel = gv_energyLevel - 1
Action(14,2, moveBackward,1,100)
BasicMove(moveStop,5)
Set_Bead(SetRCDisable,0)
gv_state = 1
}
}retry on fail
}
Sub hitObject checks how much energy remains. If we're on
our last unit of energy it resets the game state to 4 (out
of energy). Otherwise it subtracts 1 from the energy level,
moves the Spybot backwards, plays a sound and displays an
arclight animation before finally resetting the state to 1
(running) and exiting. But there are a couple of other things
here worth comment. This is the first time I've actually tried
to access the motors and you can see I've wrapped the whole
thing in a try-retry statement. This ensures that there is
no conflict with other subs trying to access the outputs.
The other thing to note is the use of SetRCDisable. SetRCDisable
controls whether a Spybot accepts controller commands. What
I've tried to do is to tell the Spybot to ignore controller
commands (Set_Bead(SetRCDisable,1)) at the top and then to
accept all controller commands at the bottom. I've done this
because you shouldn't be able to drive around casually when
you should be recovering from a hit.
The sub called from whatNext when the state = 3 is shocked:
sub shocked{
try{
Set_Bead(SetRCDisable,1)
Action(6,2,moveShake,2,100)
BasicMove(moveStop,5)
gv_shockTime = random 150
start randomShockCounter
Set_Bead(SetRCDisable,0)
gv_state = 1
} retry on fail
}
This is similar to hitObject except that it resets the gv_shockTime
value and then restarts randomShockCounter.
The sub called from whatNext when the state = 4 is loseGame:
sub loseGame{
try{
Set_Bead(SetRCDisable,1) //ignore controller
Action(sndCrash,2,moveBackward,1,100)
FancyMove(moveShake, 400)
wait 400
BasicMove(moveStop,5)
clear sound
clear display
LED[iYellowBlink] = 0
LED[iYellowWarn] = 0
Set_Bead(SetRCDisable,0)//respond to controller
stop Tasks
}retry on fail
}
Once again this is similar to the last two except that it
turns off the alert light and then shuts down the program
by using stop Tasks. Finally
we come to winGame which is called by whatNext when the game
state = 5:
sub winGame{
try{
Action(62,1, moveDance, 2,200)
wait 400
clear display
stop tasks
}retry on fail
}
As you can see winGame runs the victory dance before shutting
down the program. Which illuminates the final dark corner
of our program. Writing a program like this can at first appear
a bit daunting. But the idea is to break the job down into
small, relatively simple chunks. This will keep your code
legible and therefore easier to debug. I haven't used a lot
of comments (partly to keep a long tutorial as short as possible)
but they'll also help to remind you of why you did what. Below
is the full program. Copy and paste it into your script editor
and you should have no trouble downloading and running it
yourself.
|