How to Make a Game Like Jetpack Joyride using LevelHelper and SpriteHelper [Corona Edition] – Part 4

Bogdan Vladu

This is a post by special contributor Bogdan Vladu, an iOS application developer and aspiring game developer living in Bucharest, Romania.

Create a game like Jetpack Joyride with LevelHelper and SpriteHelper!

Create a game like Jetpack Joyride with LevelHelper and SpriteHelper!

Hello again, and welcome to the fourth and final installment of the tutorial series on LevelHelper and SpriteHelper. I said by the end of this series you would have an exciting game to play, and we’re almost there!

Last time, in Part Three, we came close to completing our game. We fully implemented collisions between the player and the other game elements, enabling the player to die and score points. We also made the player fly and added some nice sounds and animations.

But there are still a few improvements we can make. For one thing, we need a way to keep track of the score, and while we’re doing that we might as well add some cute moving bunnies that will be worth more points.

Our game looks nice, but I’ll take you through adding a background layer to give the illusion of a never-ending outdoor scene visible through the windows. We’ll make the background layer move so that the scene through the windows is always slightly different.

And wouldn’t it be interesting if we made some of our lasers rotate? Keep reading to complete the game!

Getting Started: Rotating Lasers

We’re picking up with the project as we left it at the end of Part Three. You can download that version of the project here.

The first thing we’re going to do is make our lasers rotate. We can’t have this game be too easy! So open up your project in LevelHelper, and make sure you have the latest version of your level open.

The first step is to go to Define Tags and define a new tag. Call it ROTATING_LASERS.

We will use this new tag to reference the lasers we select to rotate (not all of them… we’re not that evil, are we?) when we implement the rotation via code.

Now select a couple of the lasers in the level. Make sure the lasers you select have room to rotate. Assign the ROTATING_LASERS tag to these lasers.

Once you’re done tagging the lasers, save the level and open your gameLogic.lua. Navigate to the global variables at the top and add the following:

local rotatingLasers = nil;

Then place the following after you load the level inside “enterScene” method:

rotatingLasers = loader:spritesWithTag(LevelHelper_TAG.ROTATING_LASERS);

Here we take reference to all the sprites that have the tag ROTATING_LASERS.

Now that we’ve grabbed the lasers that we want, let’s make them rotate! Go inside your onEnterFrame method and write this at the end:

for i=1, #rotatingLasers do
	local rotLaser = rotatingLasers[i];
	rotLaser.rotation = rotLaser.rotation+1;
end

If you run the game now, the chosen lasers will rotate, but the player won’t die when it makes contact with them. This is because we haven’t registered a collision event for the ROTATING_LASERS tag. To do that, add the following inside the setupCollisionHandling method:

loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.ROTATING_LASERS, mouseLaserCollision);

Running the game now, the lasers will rotate and kill the player just as the non-rotating lasers do. Congrats, you have just added another layer of difficulty to our game. That said, you may want to play-test the game some more at this point to make sure the player can still get through the level!

If you run the game and collide with one of the rotating lasers you will notice that the player does not fall to the ground when it dies because of a collision with the laser. This is because the laser is not transformed to a sensor.

We need to add the following statement before the “if(playerIsDead == true)then statement inside mouseLaserCollsion method, so modify the mouseLaserCollision method like this:

local function mouseLaserCollision(event)
 
	local laser = event.spriteB;
 
    -- If we make the laser a sensor, the callback will be called only once - at first collision.
    -- This is not good as we want to kill the player when the laser changes to active.
    -- So we make the laser sensor, so that the player and laser don't collide, durring this collision.
    -- Then we keep track of the lasers that needs to change back to not being sensors
    -- and in the enterFrame we set back the lasers to isSensor = false
 
    laser.isSensor = true;  
    lasersThatNeedHandling[#lasersThatNeedHandling+1] = laser;
 
 
    if(playerIsDead == true)then
        return;
    end
 
    if(laser:activeFrame() ~= 1)then
	    audio.play( laserSound )
	    killPlayer();
    end
end

Moving Bunnies

Running and flying over sleeping cats and dogs and dodging rotating lasers is all fun, but let’s add another reward component to the game to give it more complexity. I have just the ticket: bunnies. Cute, helpless bunnies that our player can kill for points.

Open SpriteHelper, and go to File\New. Then open Finder and navigate to the ArtPack folder (download it from Part One if you don’t have it), select the bunnies images and drag them into the SpriteHelper window. Then click the “Pack Sprites” button.

Now select all the sprites in the list and enable the “Is Sensor” option from the Physic Properties menu. (Note that our other reward component, coins, are also sensors.)

Next, create the following two animations using the bunny frames:

Animation Name: BunnyRun
Speed: 0.400
StartAtLaunch: YES
LoopForever: YES
Frames: bunny_1, bunny2

Animation Name: BunnyDie
Speed: 0.400
StartAtLaunch: YES
LoopForever: NO
Frames: bunny__die_1, bunny_die_2

When you’re done creating the animations, save the scene inside the Images folder of your project.

Going back to LevelHelper, we can see the bunny animations inside the Animations section. Select the BunnyRun animation and drag it into your level.

We now have a bunny in the level, but it will not be visible because we need to move him with the
parallax.

To add the bunny to the parallax, go to the Parallax tab, select our parallax and add the bunny sprite to it. Set the x component of the movement ratio to 1 so that the bunny moves at the same speed as our level.

If we run the level in Scene Tester we cannot see the bunny. This is because we need to set the batch z order on the bunny image.

Navigate the the Images section and set the batch z on the bunny image to 3. This will put the bunny in front of everything except the player.

Running the level again in Scene Tester, we can see the bunny moving with the parallax, but the bunny appears to be moving in place. We want the bunny to move from right to left and from left to right. To do this, we have to make him move on a path.

Navigate to the Beziers section and click NEW to create a new bezier that will describe the path of the bunny.

Click within the level view to draw the bunny’s path, and click the Finish button when you’re happy with the bezier.

Now select the “Path” option in the bezier properties to tell LevelHelper to treat that bezier as a path.

Note:

  • By default, beziers are a simple line. You can make them true beziers by deselecting “Line” in the properties menu.
  • You can edit a bezier by pressing the Edit button and then dragging the points.
  • While editing the bezier, you can create a new point by holding COMMAND and clicking on a line of the bezier.
  • While editing the bezier you can remove a point by holding ALT (Option) and clicking on a point.

Well, now we have our path defined, but the bunny sprite doesn’t know that it should move on that path. Let’s make the bunny aware of this.

Select the bunny, then in the General Properties section, click on Path.

In the Path Settings dialogue, select the bezier that describes the path from the list, then make the motion cyclic by selecting “Cyclic Motion.”

Set the amount of time you want the bunny to take to move from one end of the bezier to the other. Then select “Flip Sprite X At Ends” so that the bunny is always facing in the direction it is moving. Otherwise, it will appear to be moving backwards half the time.

If you run the level inside Scene Tester now, the bunny should move on the path and also move with the parallax. This is cool, eh? LevelHelper is the only editor that lets you move sprites on a path and with a continuos scrolling parallax at the same time.

The last step in LevelHelper is to define a new tag called “BUNNY” and assign it to the bunny sprite. Once you’ve done this, save the level.

Time to move back to our Corona project – let’s write some code that will make the bunny die when the player collides with it.

Add the following at the end of the setupCollisionHandling method:

loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.BUNNY, mouseBunnyCollision);

This informs LevelHelper of the collision callback, but we also need to define the method for the callback. Add the following before the setupCollisionHandling method:

function mouseBunnyCollision(event)    
    if(playerIsDead)then
        return;
    end
 
    local bunny = event.spriteB;
 
    if(nil ~= bunny)then
 
    	if(bunny.sequence == "bunnyRun")then
	        scoreHitAtPosition({x = bunny.x, y = bunny.y}, 500);
			audio.play(bunnyHitSound);
			bunny:startAnimationWithUniqueName("bunnyDie")
			bunny.lhPathNode:setPaused(true)
        end
 
    end
end

In the above code, we test for a dead player and do nothing if that’s the case. We then take the bunny sprite from the contact and test if it’s valid and whether it’s running the “bunnyRun” animation.

If the bunny sprite meets both conditions, we give 500 points to the user and play the bunny hit sound (part of the sound pack downloaded in Part Three of this series). We then start the bunnyDie animation on the bunny sprite and stop the sprite’s movement on the path.

Since we are starting an animation on the bunny lets register that animation. Add the following where you register the animations:

animationMgr:registerAnimationWithNameOnSpritesWithTag("bunnyDie", LevelHelper_TAG.BUNNY);

As you can see in this case we are registering the “bunnyDie” animation on all the sprites that have the tag BUNNY, in our case all the bunnies.

If you run the game now, you’ll see that the bunny dies when he collides with the mouse. But if you loop through the entire level, you’ll see that the bunny you killed doesn’t reappear in the level: it stays dead! So let’s do the same thing we did with the coins in Part Three and reset the bunny.

Find your spriteInParallaxHasReset method and modify it to look like this:

function spriteInParallaxHasReset(sprite)
    if(LevelHelper_TAG.COIN == sprite.lhTag)then
		sprite.alpha = 1;
    elseif(LevelHelper_TAG.BUNNY == sprite.lhTag)then
		sprite:startAnimationWithUniqueName("bunnyRun");
		sprite.lhPathNode:setPaused(false);
    end    
end

Above, we simply check for the BUNNY tag, and when we find a sprite that is a bunny, we set its animation to bunnyRun and unpause its path movement.

The complete project up to this point can be downloaded here.

Displaying the Score

We’re going to implement a dynamic way of showing the score. Instead of having a fixed display at the top or bottom of the screen, we’re going to show the score in a temporary pop-up window each time the user scores.

Begin by adding the following at the top of gameLogic.lua

local scoreText = nil;

Now define this new method just after scoreHitAtPosition method

function setupScore()
    score = 0;
    scoreText = display.newText("Score: 0", 0, 0, native.systemFont, 22)
    scoreText:setReferencePoint(display.TopLeftReferencePoint);
    scoreText.x = 40
    scoreText:setTextColor(255, 255, 255)    
    globalGroup:insert(scoreText)
end

Here we create a new label and assign it to our scoreText variable. We then add this label to the global group.

Now call this new method inside the “enterScene” method right after the call to setupCollisionHandling:

setupScore()

This is good, but right now we’re not showing the actual score to the user. We’re just showing some text. Let’s make the text change when the user gets points.

Modify your scoreHitAtPosition method to look like this:

function scoreHitAtPosition(position, points)  
    score = score + points;
    scoreText.text = "Score " .. tostring(score);
 
    tempPoint = display.newText(tostring(points), 0, 0, native.systemFont, 22)
    tempPoint.x = position.x
    tempPoint.y = position.y
	tempPoint:setTextColor(255, 255, 255)    
 
    local hideTempText = function( obj )
	    obj:removeSelf()
	end 
	transition.to( tempPoint, { time=1000, alpha=0, x=tempPoint.x, y=tempPoint.y - 50, onComplete=hideTempText } )
 
end

Here we add the new points to the score variable. Then we set the text of our score label to be the newly updated score.

We then create a new label that will be displayed at the position we get in the attribute of this method. As shown in the code, this is the position of the coin or bunny triggering the score update. The coin or bunny vanishes upon collision with the player, and a score display temporarily appears in its place.

We then create a transition to move the text up and make it disappear. We also assign a callback method when the transition ends so that we can remove the text from the screen.

Run the game now and see how nicely this works. Every time the player makes contact with a coin or a bunny, a label pops up showing the updated score, displays for one second, then disappears.

Score displayed in Rocket Mouse

Adding An Outside Layer

Now for our last tweak: an “outdoor” scene that will be visible scrolling through the windows in the level, adding a nice sense of depth to the gameplay.

Open SpriteHelper and create a new scene with File\New. Then open Finder, navigate to the ArtPack folder, select the bgWindow, window_background, and window_foreground images, and drag then on the SpriteHelper window.

Press Pack Sprites to pack the new sprites. Because one of the images doesn’t have alpha, a new window will ask you if you want to crop the image using a selected color. Choose Don’t Crop.

Select all the sprites and set the “No Physics” option from the Physics tab. Save the scene as “outside” and place it in the Images folder.

Back in LevelHelper, let’s create a new level in order to see the steps. With level03 opened, press Command-Shift-S to save the level as a new level. Give it the name level04 and save it inside the Levels folder.

Now inside the level view, select all the window sprites as well as all the background sprites from under the windows.

Delete the sprite by pressing the Delete key or clicking the red minus sign button on the left.

After the removal of these sprites, the scene should look something like this:

Double-check that you’ve removed all of the window and under-window background sprites, and save the scene.

Now filter the outside sprites by checking the “filter” option next to outside.png. This will make it so that we only see the sprites from that image in the list above. Set the batch z order for outside.png to -1 to put all the sprites in outside.png behind everything else.

Next, drag the bgWindow sprite and place it in the empty spaces left when you removed the windows and background sprites.

When you’re ready, select all the new sprites with the window cut (all bgWindow sprites) and set their Z Order to 1.

Add these new sprites to the parallax and set the x component of the movement ratio to 1. Save the level.

Press the Test Level button to run the scene in Scene Tester. You may notice some of the new sprites have “noise.” That’s because some of the positions of the new sprites aren’t lining up correctly with the old sprites.

There are two ways to fix this problem:

  1. Adjust every mismatch individually by selecting each sprite and moving the sprite using the arrow keys until it lines up correctly with the other sprites.
  2. Select all of the misaligned sprites and set Scale X in the General Properties menu to 1.01, or a value that cancels the noise.

Save the level when you’re satisfied with how the sprites are positioned.

Now that everything is in place, let’s create a background that will move at a different speed from the rest of the parallax.

First let’s lock the sprites already in the level so that we don’t accidentally move them. To do this, select all the sprites from the list on the left, and click the Lock button.

Note: To unlock sprites, select them in the list and press the Lock button again.

With the sprites locked, drag the window_background sprite into the scene and place it above the main screen. Make sure that the left vertical border of the window_background sprite is lined up exactly with that of the main screen.

Now make sure the window_background sprite is selected, and duplicate it to the right until the end of the game world by using the green + button (or pressing Command-D). Make sure that you have the correct anchor point selected.

The level should look like this when you’re finished:

Now select all the new window_background sprites and add them to the parallax. Set the x component of the movement ratio to 0.3.

Repeat this process for the window_foreground sprite.

Drag it into the scene and position it above and in alignment with the window_background sprites:

Clone it until the end of the game world:

Add it to the parallax and set the x component of the movement ratio to 0.7:

Now select all the window_background sprites and drag them down onto the main scene so that you can see them through the window.

Repeat the process for the window_foreground sprites. Drag all of the sprites down until you can see them through the window.

Save the level when you’re done.

Running Scene Tester now, you should see the outside world moving at a different speed than the inside world. This makes it seem to the user that they are never seeing quite the same outside image through the window, making the game world feel more three-dimensionally “real.”

Inside gameLogic.lua, change the loading level code so that it loads the level04 file:

loader = LevelHelperLoader:initWithContentOfFile("level04.plhs")

There’s nothing left but to run the game and enjoy!

Rocket Mouse final project!

Where to Go From Here?

Congratulations, you’ve finished the game! It’s been a long journey. I hope you enjoyed creating this wonderful game with me, and hardly noticed you were learning in the process. Thanks for reading!

An example project with all of the code from the entire tutorial series can be downloaded here.

As always, I am available to answer questions in the forums for this tutorial series, both here and on the LevelHelper forum.


This is a post by special contributor Bogdan Vladu, an iOS application developer and aspiring game developer living in Bucharest, Romania.

User Comments

30 Comments

[ 1 , 2 ]
  • In your tutorial, when you want to register the collision between the bunny and the player, you write:

    Code: Select all
    loader:registerPreColisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.BUNNY, mouseBunnyCollision);


    This is incorrect. I was not able to detect a collision between the two objects when testing this code.
    But after reading your project files, I have noticed that you did change the code in your project files to :

    Code: Select all
    loader:registerBeginOrEndCollisionCallbackBetweenTags(LevelHelper_TAG.PLAYER, LevelHelper_TAG.BUNNY, mouseBunnyCollision);


    But you forgot to change it also in your tutorial ^^ Just advising, so you can correct that mistake.

    Thank you

    Ray
    rayman1900
  • Thanks. I will tell Ray to fix it.
    Also - i like to take this opportunity to tell you that in the latest version of supporting code parallax has been improved giving a more then twice better performance. However this fix will require that you set a much bigger speed to it inside Levelhelper - so what used to be 0.2 now its 200

    This change is only available in the code for the latest daily build. So soon it will be available for everybody.
    Cheers,
    Bogdan Vladu
    vladubogdan
  • Hello Bogdan!

    As I told before, congrats for this great stuff!
    You explain very well all the steps to complete an app using LH and SH (I thing they are very useful!)

    I have a problem adding the path to the bunny: I run the tester and I see the bunny keeps stopped and don't move on the path. I do all the things you told in the tutorial but the bunny doesn't move across the scene.

    Thanks again for you great job!
    alberto.pm9
  • Is the bunny added to the parallax? Also is the move with delta selected in the path property window? Also make sure the bunny is not dynamic.
    vladubogdan
  • Yes, the bunny is added to the parallax, I tried with delta movement selected and unselected, and the bunny animation is set as static at SH.
    alberto.pm9
  • It should be with delta. I can have a look if you send the project vladu dot bogdan @ gmail dot com
    vladubogdan
  • I've completed tutorial, (realyy nice job!!) but I've got problem with the background sound.
    The same issue is with the project downloaded from your website.
    Corona: Build: 2012.818
    [..\..\..\..\src\libmpg123\parse.c:529] error: Giving up searching valid MPEG he
    ader after (over) 64K of junk.
    [..\..\..\..\src\libmpg123\parse.c:1018] error: Frame size too big: 21166
    [..\..\..\..\src\libmpg123\parse.c:529] error: Giving up searching valid MPEG he
    ader after (over) 64K of junk.
    WARNING: Failed to create audio sound(Music/backgroundMusic.m4a)


    I can open background sound in Windows Media Player without any problems.
    Any idea how to deal with it ??
    Kamil
  • Hi BogDan,

    Thank you very much for your great tutorial.

    I just started using with LevelHelper 1.4.9 and had no experience with previous version of LH.
    I got stucked when I tried to follow the steps on creating Bezier Path for bunny. LevelHelper 1.4.9 UI is very different from previous version. I was able to create a Bezier Path but not sure how to connect it to the bunny.

    Also, how do I drag a animation to level screen. I am unable see animations panel in LH 1.4.9?

    Can you please shed some lights? Is that any documentation for this?

    Your help is much appreciated.
    jworld3
  • Hey,

    Please see attached screenshots

    Screen Shot 2012-09-03 at 9.36.52 AM.png


    Screen Shot 2012-09-03 at 9.37.04 AM.png


    Screen Shot 2012-09-03 at 9.39.13 AM.png
    vladubogdan
  • Hi Bogdan,

    Thanks for you prompt reply. The images you attached I believe are for my second question. Can you please answer my first question: how to connect a bezier path to the bunny?
    I created a bezier path and don't know how to connect to bunny. At bunny's bezier properties, it's all greyed out. How do I select a bezier path for the bunny?

    Attached please see my current project settings. Thanks in advance for your helps again.

    KC
    jworld3
  • It was for the path movement

    I made this movie for you here

    http://dl.dropbox.com/u/27229440/PathMovement.mov
    vladubogdan
  • Hi Bogdan,

    Thanks again for your speedy response and video. You are totally awesome.

    Path checkbox in Bezier properties is ENABLED after I updated LevelHelper to version 1.4.956.

    After that I was able to assign Bezier node to the bunny. Thanks!
    jworld3
  • Hi Bogdan,

    I have issue on setPaused() method call below. It is giving error.

    bunny.lhPathNode:setPaused(true)

    ERROR:

    .eng/Desktop/CoronaProjects/RocketMouse/gameLogic.lua:181: attempt to call method 'setPaused' (a nil value)


    Thanks.
    jworld3
  • Here is the list of functions added to a corona sprite (display object) by levelhelper

    They are defined like
    "coronaSprite.transformScaleX = transformScaleX"

    they should be used like

    coronaSprite:transformScaleX(0.5);

    the method name from the left should be used.

    So for you

    coronaSprite:pausePathMovement();


    Sorry, for the lack of documentation on this. Documentation will be released this week.



    --LevelHelper functions - transformations
    ----------------------------------------------------------------------------
    coronaSprite.transformScaleX = transformScaleX;
    coronaSprite.transformScaleY = transformScaleY;
    coronaSprite.transformScale = transformScale;


    --LevelHelper functions - animations
    ----------------------------------------------------------------------------
    coronaSprite.prepareAnimationNamed = prepareAnimationNamed;
    coronaSprite.playAnimation = playAnimation;
    coronaSprite.pauseAnimation = pauseAnimation;
    coronaSprite.currentAnimationFrame = currentAnimationFrame;
    coronaSprite.setAnimationFrame = setAnimationFrame;
    coronaSprite.restartAnimation = restartAnimation;
    coronaSprite.isAnimationPaused = isAnimationPaused
    coronaSprite.animationName = animationName
    coronaSprite.numberOfFrames = numberOfFrames
    coronaSprite.setNextFrame = setNextFrame
    coronaSprite.setPreviousFrame = setPreviousFrame
    coronaSprite.setNextFrameAndLoop = setNextFrameAndLoop
    coronaSprite.setPreviousFrameAndLoop= setPreviousFrameAndLoop
    coronaSprite.isAnimationAtLastFrame = isAnimationAtLastFrame;


    --LevelHelper functions - path movement
    ----------------------------------------------------------------------------
    coronaSprite.prepareMovementOnPathWithUniqueName = prepareMovementOnPathWithUniqueName
    coronaSprite.pathUniqueName = pathUniqueName
    coronaSprite.startPathMovement = startPathMovement
    coronaSprite.pausePathMovement = pausePathMovement
    coronaSprite.isPathMovementPaused = isPathMovementPaused
    coronaSprite.restartPathMovement = restartPathMovement
    coronaSprite.stopPathMovement = stopPathMovement
    coronaSprite.setPathMovementSpeed = setPathMovementSpeed
    coronaSprite.pathMovementSpeed = pathMovementSpeed
    coronaSprite.setPathMovementStartPoint = setPathMovementStartPoint
    coronaSprite.pathMovementStartPoint = pathMovementStartPoint
    coronaSprite.setPathMovementIsCyclic = setPathMovementIsCyclic
    coronaSprite.pathMovementIsCyclic = pathMovementIsCyclic
    coronaSprite.setPathMovementRestartsAtOtherEnd = setPathMovementRestartsAtOtherEnd
    coronaSprite.pathMovementRestartsAtOtherEnd = pathMovementRestartsAtOtherEnd
    coronaSprite.setPathMovementOrientation = setPathMovementOrientation
    coronaSprite.pathMovementOrientation = pathMovementOrientation
    coronaSprite.setPathMovementFlipXAtEnd = setPathMovementFlipXAtEnd
    coronaSprite.pathMovementFlipXAtEnd = pathMovementFlipXAtEnd
    coronaSprite.setPathMovementFlipYAtEnd = setPathMovementFlipYAtEnd
    coronaSprite.pathMovementFlipYAtEnd = pathMovementFlipYAtEnd
    coronaSprite.setPathMovementRelative = setPathMovementRelative
    coronaSprite.pathMovementRelative = pathMovementRelative
    coronaSprite.pathMovementCurrentPoint = pathMovementCurrentPoint;

    --LevelHelper functions - joints
    ----------------------------------------------------------------------------
    coronaSprite.jointsList
    coronaSprite.jointWithUniqueName
    coronaSprite.removeAllAttachedJoints
    coronaSprite.removeJoint
    vladubogdan
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Gaming!

    Loading ... Loading ...

Last week's winner: Apple TestFlight Tutorial.

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in January: WatchKit.

Sign Up - January

Our Books

Our Team

Tutorial Team

  • Matt Galloway
  • Kirill Muzykov

... 60 total!

Update Team

  • Ray Fix

... 12 total!

Editorial Team

  • John Clem
  • Alexis Gallagher

... 17 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

  • Richard Casey

... 4 total!