An Analysis of Level Designs in Spring It

I wanted to put Spring it, the game I made in 2015, on to Steam.  I think its puzzle style lends itself well to the PC platform and with only a few minor tweaks, we would have it playing well on PC.

Unfortunately, upon playing through it again, and remembering some of the criticism we received previously,  I realized there were some changes that we needed to implement.  I really wanted to tackle the difficulty curve that we had in the game.  The game is hard.  We do not do an appropriate job teaching players about the game before they are thrown into the difficult puzzles.  A lot of this stems from lack of play tests during development and the level of comfort we, the developers, all obtained with the basic mechanics while developing the game.

I went through each of the first 10 levels to analyze what we assumed the players knew and what we expected them to learn.  I then wrote down wrote some of my initial thoughts.  

Tutorial

Assumption: Players know nothing about the game.

Players Learn: They can drag a spring into the game from the UI in the bottom left

Problems: We overlooked the fact that people did not know they could drag springs on top of conveyors and that the conveyors would then hide.  This was probably our biggest failure for the design of the game due to us making the assumption they knew how to do this the entire game.

Level 1 

Level 1 SPring In Action.png

Assumption:  Players learned that they can drag a spring into the scene from the UI and those spring can be placed on top of conveyors. Players do this 3 times to reinforce this action.

Players Learn:  How to better drag springs, Maybe eve how far the springs will launch a robot horizontally.

Problems: Players can still complete the level without ever learning that springs can be placed on conveyors.

Level 2

In this level players need to deal with the aspect that robots launched from springs have a height component as well as their horizontal one.  Player also must place springs on both the floor and over top of conveyors. This is the first level where players can use less springs than they are provided to obtain a better end level score.

Assumptions:  Same knowledge as last level, just improved comfort with actual mechanics of placement.

Players learn: If they use one too many springs, they will only get two bolts instead of the usual three. (I feel this is a bit too early to teach the players the aspect of mastery)

Level 3

Level 3 Spring It Design Blog.png

Assumption:  Players know they can place springs over top of the conveyors and that this hides the conveyor, springs can be placed on both floor and conveyor. Same assumptions as levels 1 and 2.

Players Learn: Reverse conveyor cause robots to move int he opposite direction and how to solve the 3 length gap problem with a spring (players need to place a spring in the middle of a 3 length gap on the floor).  

Just looking at these first three levels, I can tell there is too much being thrown at the player and we did not really analyze what they were doing at each step.  This gave me a better idea for how to reorganize the flow of the game levels.

Proposed solution

Have the tutorial level start with the three length gap problem and teach the players just to place springs into the level on the floor.  This way we are only teaching placement rather than both placement and replacing conveyors and we are embracing the what the player's inutively thought, that springs cannot replace conveyors.

Level 1 is then modified slightly so the players place the springs on the floor.

Level 2 is where we teach that springs can be place on top of conveyors to replace them

Level 3 is the previous level 2 so the players can practice placing springs on the floor and over top of conveyors.

Just this simple change should ease players into the game play more comfortably than before while still keeping in mind how people play the game when first starting out.

The Remaining 7 levels

The biggest shock the players face is when they see level 10. 

Level 10 is daunting.  It has almost every previous mechanic presented being used but to a new player, these all feel like unfamiliar mechanics.  Rather than a test or boss fight combining all that they learned, it appears as a string of entirely new problems.

Here I listed all the mechanics and problem we were trying to introduce up to this point along with their quantity present in this level:

  • Doors - 2
  • Magnet - 1
  • Cannon - 1
  • Furnace gap - 1
  • Fans - 0
  • Moving an object after it was placed to continue to progress a level - 1
  • Reverse conveyors - 1 set (I am only counting the upper length because they obstruct the player's progression while the lower set helps the player)

Now the above list can be daunting but is expected of a test level.  So the level's design is not necessarily the problem.  The problem is preparation leading up to the level itself.

Looking at the previous 10 levels, I created a list tallying the number of times each mechanic was used in the first 9 levels:

  • Doors - 1
  • Magnet - 1
  • Cannon - 1
  • Furnace - 3 (but only 1 Furnace gap)
  • Fans 2
  • Moving an Object after it was placed to continue to progress a level - 1 
  • Reverse Conveyors - 3

As you can tell although players have seen all of these mechanics before, I would argue that only the reverse conveyors and the furnaces are problems the players are familiar with.  So we, as developers, did not provide enough practice leading up to this level, and as a result this level comes across as daunting and near impossible.

Conclusion

This is not a post I would have been able to write when we released Spring It! in 2015. I just did not have the correct mindset.  Due to my involvement with the development itself, certain actions, like dragging objects into the level and the functionality of the mechanics were already second nature to me.  So when I personally played the levels, I only played them to make sure I was still being challenged, if only slightly, and that the levels themselves were interesting.

The obvious way to prevent this problem would have been, play test more often and more efficiently, but that is way easier said than done.  It can be difficult to find frequent quality play testers.  I am writing this to present a mindset or methodology to think about your levels in a way to better teach your players how to play your game.  Now this will not replace good play tests, but it will narrow down what you are focusing on when you do play test, so you can better analyze the results and maybe even catch things before you start testing.

For each level, try and only teach the player 1 new thing.  Write what the goal of the level is, along with what you assume they will have known at this point.  When you watch them play you can see if your assumptions hold true.  If they do not, you needed to provide them more or better practice previously.   Next you can analyze the level itself to ensure it is properly teaching the players what you wanted.  

People talk about interest curves for player excitement but I think it is important to reference them when teaching new mechanics as well.  The peaks should be your new mechanics being introduced, while the troughs are the reinforcement of those mechanics.  I included a image of Star Wars: A New Hope's interest curve as a reference because I frequently use it myself.

pacing-example.gif

A Second look at the Parasite Style Support

Back in 2014 I made a post about a support for League of Legends and I wanted to take a second look at that champion here.  

The goals of these changes are to:

  •  firmly identify the champion as a support
  • Provide more room for mastery
  • Provide more interesting choices for the player

The main abilities I looked at changing are W:Feed/Nourish and the Passive Adaptation.  I also had smaller tweaks to E: Paralyzing injection and the Ultimate Find Host!

I am thematically happy with the Q:Weaken and its changes would come from playtesting to see if the correct stats are being siphoned from enemies and in the correct amounts.

Find Host!

  • Mana: 100
  • Cast Time: 0.5 sec
  • Required Jump Time: 6/7/8
  • Range: 1000
  • Radius: 175
  • Cooldown: 60/45/30

This champion launches itself in a toward a targeted area of radius 175.  This champion will attach to the champion closest to the center of the targeting circle.  If there are no champions present in the targeted area, this abilities cooldown.  When this Champion hits another champion, that champion becomes the host.

While attached to a Host, this champion cannot move.  This champion will stay with the host through all speed boosts, jumps, Invisibility,  Teleport, and Flash.  They will not turn invisible with the champion that goes invisible.  For specific Champion ability interactions, please consult the list in the additional notes below.

(New) While this ability is going, this champion must jump off of its host before the required Jump Time or this ability will go on cooldown and this champion will be stunned for 0.5 seconds. 

  • If host champion is an ally, this champion attaches to their back.  All skillshot and damage coming at the host’s back is instead hits this champion
  • If host champion is an enemy, this champion attaches to their face.  All of their auto attacks instead hit this champion.

While attached this champion  will not be affected by:

  • Blinds
  • Entangles
  • Forced Actions
  • Slows
  • Roots
  • Any Cleanse
  • Any Knock Asides
  • Some Knockups
  • Some Knockbacks

This champion can be removed from their host after:

  • End of the Ultimate
  • Any stun
  • All Flings/Grabs except Diana’s Moonfall and Darius’s Apprehend.
  • Any knockback attack that displaces this champion by 300 units or more.
  • Any Suppression.
  • Any Polymorph.
  • This Champion’s Death
  • Death of the Host

Additional Notes:

While travelling between champions this champion is untargetable and cannot be interrupted by anything other than death.  This should allow the champion to use the second jump as an escape from potentially being detached.

Design Choices

This ability was changed to feel more like a changed state for the champion rather than a huge game changing ability.  I want the player to be able to use this ability during every major fight so that their fight pattern is a choice between when they need to use their abilities in champion form, and when they best help there team in parasite form. This play pattern evolves even further when the player now must choose who and when they need to choose for their host within a teamfight. This removal of the duration and the general CD reduction both help open up more options to the player and make for a more interesting champion.

Finally, the player is able to jump off of a host, choosing to end the parasite state sooner, and is rewarded with a shorter Cooldown.

E: Paralyzing Injection

  • Mana Cost: 40
  • Damage: 25/ 35/45/55/65 (+10% AP)
  • Effect: 17%/20%/25%/30%/35%
  • Cooldown: 1.5 seconds
  • Range: 600 (50 lower than standard attack range)

This champion fires a short range skillshot.  Any enemy champion hit by this injection has their movement speed reduced for 3 seconds.  This ability stacks until the total debuff would reduce the target’s movement speed by 100%.  

(New) The cost of the next injection is increased by 50% for the next 8 seconds upon cast.  This effect stacks. 

(New)  When this effect expires on the champion, that champion becomes immune to this ability for 5 seconds.

Find Host Changes:

If the Host is an enemy, this ability also reduces the attack speed as well.  If this ability reduces the champion’s attack speed by 100% they are instead stunned. This allows the host to become stunlocked if left alone for too long.  

If the Host is an ally, this ability changes to:

Adrenaline shot

  • Cost: 40
  • Effect: 10%/12.5%/15%/17.5%/20%
  • Cooldown: 1.5 seconds
  • Duration 2 seconds

The Host gains attack speed and movement speed for 2 seconds.  This effect stacks.  Upon reaching 100%, this champion is stunned for .5 seconds and the host gains a 50% damage increase for 2 seconds.

Design Choices:  
I wanted a CC ability for this champion but also something that fit the flavor of a parasite inhibiting their host.  An ability that slowly builds up into something very dangerous was what I was aiming for in feel.  AddtionalIy the ramping up mana cost was added to give a bigger commitment to the player when they choose to engage.  Finally, the immunity added when the debuff expires was added so that if the player choose not to replenish their slow stacks, the enemy has a larger window of opportunity to strike back against this champion.

As for the change Adrenaline Shot, With the CC immunity spell moving over to the W, I felt there was more room to buff the allied host in this situation.  I thematically went the direct opposite of a paralyzing injection but tuned the numbers slightly.  

Nourish was very nice in keeping the carries alive, but I felt this champion could be more interesting while still providing the same function. The attack speed is nice, but the movement speed is what will help keep the carries safe, this also allows for a higher level of mastery for your team with this champion.

Passive: Adaptation

This champion’s stats change based on other champion distance to it.  If an ally is nearby, this champion shifts into defensive mode and gains armor and magic resist.  If an enemy is the closest champion to this champion, this champion shifts into offensive mode and gains additional Move Speed and slow resistance.  This champion can only swap shift between modes once every 5 seconds.  The values will still change, but the mode will not.

Ally proximity is always prioritized over enemy proximity.

Design Choice:

My intent with this champion was to be a defensive support.  Slow the enemy, block hits, and stay alive.  This was to provide additional damage reduction and create threat when charging and attaching to an enemy.  The MS and slow reduction was chosen because I wanted offensive mode to provide more threat.  The MS and slow reduction gives more chase down potential when trying to catch the enemy with a paralyzing injection chain.

The shift CD was added to give more control to the player.  This allows the players to plan to shift into Offensive mode when they need to chase someone down.  The shift will only occur when the player gets closer to the enemy than their ally, and then they have at least a 5 second window where they can chase.  But this shift also gives a predictable window of opportunity to the enemy where this champion is much easier to kill due to the loss in free armor.

W: Absorb

  • Mana Cost: 60
  • Range: Self
  • Cooldown: 20/18.5/17/15.5/14
  • Cast Time 0.25

This champion braces and absorbs the next damaging ability hitting it.  The champion then gains a shield for 5 seconds, absorbing the damage type it was just dealt. The ability’s effects are still applied, but the shield is applied before the damage is dealt.

If the damage was physical, the shield will absorb physical damage but the champion will take additional damage from magical sources while the shield is present. If the damage was magical, visa versus. If the damage was true damage than the shield will absorb all damage and there will be no damage magnification.

Find Host Changes:

If the Host is an enemy this ability changes to:

Release:

The champion analyzes the next ability that hits its host, causing its host to take additional damage from that type of damage for the next 6 seconds, regardless of whether this champion is still attached.

If the Host is an ally, this ability changes to:

This champion blocks the next incoming attack to its host, taking the damage instead.  All abilities that deal damage of the absorbed ability’s type to the host, are instead dealt to this champion for the next 5 seconds.  This effect ends after 5 seconds or when Find Host ends.

The next (base value + % max health) damage of that type taken by its host is instead taken by this champion.  This effect ends when Find Host ends.

Design Choices:

I felt like feed was a jungle or solo lane ability.  The AoE damage would hurt the bottom lane’s wave control in most solo queue games, and using the heal did not really offer any interesting decisions for the player.  It was just, find many minions or champs and heal when nearby.

I still wanted to give this character some type of survivability but I wanted to follow up with the theme of adapting.  I felt that a shield would help survivability against poke comps, although, engage comps are really where he would shine, and it offered an interesting choice to the player.  This would act as a nice damage reduction and a good tanking tool, but also had the drawback of leaving yourself more exposed to other champions.  This, offers the enemy a nice chance for counterplay.  The ability’s trigger would be noticeable by the change in color of this champion’s health bar and they could adjust their focus to attack this champion to kill it faster.  But is this champion really the biggest threat at the time, is still up to the players to determine.

Overview

With these changes, the champion is firmly placed in the support role as the champion has no wave clear, is mostly defensive in stats, and has a strong CC ability.  Next, the mastery of this champion increased significantly with the increase duration and options on the Ultimate, the timing mechanics on W's absorption, and finally, the shifting modes for the passive.  Before w and the passive were both very passive abilities, now they both have at least some active component with the W being a very skill based ability.  Both of these abilities allow for a bit more player skill to be involved.  

I am happy with the current state of this champion and the decisions to be made around the Find Host and the timing required for each of the abilities.  There is a lot of mastery to be had and this is a champion I would want to play.

Spring It Postmortem: Production and Business

Looking back on the release of Spring It as the producer I can see a lot of things we could improve upon.  I want to talk about those things here.

Pipeline Lessons Learned

Awhile ago I look a look at the time we spent on each aspect of the project to look for inefficiencies and to improve our overall decision making process.  The biggest two inefficiencies was a lot of wasted time on art assets and programming features that were created but never used.  Both of these problems stemmed in part from a poor design direction regarding everything besides the core gameplay loop, and the other part due to not play testing enough.  

First, the art issues. We had Bongi model up objects whenever we decided to start working on a new feature.  There was not approval process, and very little direction to how the object looked.  This lead to tons of models being completed just for us to say we don't like they way they look.  This was not an acceptable use of our limited development time.  To solve this, we first started requiring all art to have concept art first, to then be approved by the team. This saved us a ton of time and from that point forward we had only minor adjustments to our models.

Secondly, I implemented a few features, like the Level Select screen or the trap conveyor that we never actually ended up using the code from.  We also learned that a lot of the features were not as fun to use as they could be.  We were not running playtests as often as we should have been and we were not planning around playtests with our development.  Once we started focusing on playtests, and what we could learn from the playtests, we were able to have clear goals for each development cycle.  This meant, as a programmer, I was only focusing on the most important thing as each step of the project.  For all future projects, I will be much more playtest/milestone focused in my sprints and planning.  Additionally, art will only be added after a function has be internally playtested and approved and concept art has been approved.

Project Tracking

The project started out as a very scrum like tracking system with the meetings as well.  This was too cumbersome for us so it evolved significantly over the time we were working on the project.  I still kept track of hours, and we as a team estimated the hours but the meetings we had changed dramatically.  Instead of being scrum meetings, we just had a team meeting to discuss all aspect of the project once a week. This proved to be more useful to us than scrum meetings twice a week. We really did not have enough time during the week to make measurable progress to constitute more than one full team meeting a week. 

Google sheets was the tool I choose to use for the first scrum tracking, but I am thinking using something closer to Trello.com would be easier for all future projects.  I recently started using favro.com based on a talk at GDC, and I plan to implement our next games tracking using this.  The main reason is because we really lacked a way to tell each other when to test each feature, and a lot or the time our internal testing was sub par.  I plan to use the not started, doing, testing, done categories that trello and favro type systems strive in.

Post Completion and Release

One of the things I knew the least about going into this project was the marketing and release aspects of the game.  The game finished over a month ago, and due to some procrastination and self rewarded relaxation we are only just releasing the iOS version now.  The approval process and the materials required for marketing take a lot more time than I originally expected.  The good news is that I now more about how to plan for this time, and the approval processes for the various distributors.  I know now that there is still a large amount of work that needs to be done once a game has been completed.

Spring It Postmortem: Programming

 

After releasing my first game, I still do not feel like a programmer.  Real programmers are the people who obsess over efficient code, a real programmer would have done this whole project faster, a real programmer would not have any of the bugs Spring it currently has,  Right?

I know all of the 100+ scripts in Spring It, I can easily debug the problems we experienced, yet the game still has bugs.  Every time I play the game, each bug reminds me that the game, that I, could be better.    One of the saving graces of playing the game, is that it is still fun and after a few levels, I can quickly ignore the bugs.  Realizing that I can honestly say I am proud of the game.

But now, I would like to take a look at some of the systems I implemented that could have been done better, ignored all together, and finally ones that I feel were done really well.

Scripts to be improved

The script that Spring It uses to build the levels from Tiled has a few rules that make it very particular to work with.  As of right now, it can only support having one collector in each level and that collector cannot have anything underneath it.  This isn't a problem for any of our current levels but we could definitely explore that design opportunity if this game goes any further.  Also to that point.  Our collectors and spawner scripts and models are very limited in their implementation.  The spawner can only face right and the collector can only collect from the left.  This really limited level flow and can lead to many levels feeling the same.  If we do plan to expand upon this project in the future, this will one of the first changes I implement to the project.

A few minor scripts that were quickly implemented near the end of the project were the credits auto-scroll, and the script to place the background that can be scene through the window.  Both of these could have used more time to create a slightly more robust system that could be more versatile.  They work for what they are needed for right now, so they will remain that way until I need to improve upon them.

Should have been ignored

I made a post back in December called Revamping the UI: Select Your Level. This who process, while useful as a learning exercise, is not longer used in the game.  We thought to make a really cool Level Select screen, but ultimately it was more work than it was worth.  after we had a second artist look at it, his design was better and much easier to implement.  I think this mistake was ultimately a lack of design direction for elements outside of the game's core entertainment loop.  We will be watching for this problem, as it caused multiple problems throughout development, when we create our next game.

The next system was something I thought would be really cool and would end up saving us work in the long run.  I modified the Unity editor to create tutorial pages for us and track all of the currently created pages. It was harder to implement that I thought it would be, but even more, I learned that tutorial pages need a personal design touch to make them actually look good.  So we ended up ignoring this tool all together and creating the pages by hand.  This was my fault for relying too much on tools.  I should have designed a few tutorial pages first, and then see if they could have been implemented easily by the unity editor.

Time for a pat on the back

The are some scripts that I am really happy with their implementation.  They are flexible and/or small all while getting the job done.  The first two scripts that really stand out are the VFX and Audio Containers that I made.  These containers can be put on any object in Spring It and they will handle all VFX or audio that needs to be associated with that game object.  The scripts themselves are clear and the code is easily changed and added to.  These are definitely scripts I would like to emulate in later projects.

The next system that was very flexible and easy to work with was my centralized Event system class.  This is talked about in detail in my post Revamping the UI: Consider Events Handled, so I will not go into detail about its function here.  The main point I have about it is that it allowed me to go back and add new events very easily. It also allowed me to search through all my current events just by typing eventHandler into Monodevelop.  These two things really benefited me near the end of the project when I needed to come up with quick solutions to the bugs we were experiencing.

Finally,  despite talking about this system in some regard in the things to improve category, I am actually very happy with the scripts I use to build the levels for the game.  It allowed Jeremy to easily update and change levels as he was designing them; based on his feedback I believe the system saved him a large amount of time design wise.

Although I do not feel like a real programmer, looking back at what I have accomplished, this feeling most likely stems from the fact that I now know enough about programming to realize how much more there is still left to know.  My next game will be smoother in implementation and I plan to apply a lot of what I have learned making Spring It.

Mistakes were made: how to make debugging easier

I had just finished dynamically placing windows onto the background of the level. To avoid working with shaders, I planned to program a script that will dynamically build the background around the levels.  Below you can see what I was aiming to create.

Finished Background

I planned out the entire code,  separated each smaller task into a function, and clearly defined my members variables.  The list I wrote is below:

Notes for what I want to do
        - Find all spaces that are valid and cover them with a background.
        - Scale the background to that size

        Steps:
        1. Find first valid space
        2. Find Invalid space in that row
        3. Measure area between spaces and save coordinates of space
        4. Go to next row
        5. measure up to same distance, if invalid space stop
        6. Spawn Column
        7. Make all those spaces invalid
        8. Go back to step 1 until all spaces are invalid


    Supporting functions Required
        - Spawn 1 wall with texture
        - Generate new texture
        - Keep track of scaling and offset of texture

This was were I made my mistake.  I programmed all the individual functions, then connected them all, then watched as nothing was working the way I planned.  I spent the next 6 to 8 hours debugging the entire program at once, because I had thought I understood what was wrong. It turned out that multiple different functions all had minor issues:  Inequality symbols facing the wrong way,  functions ordered incorrectly, and incorrect variables being used.  I only discovered these issues once I finally started looking at each function individually and analyzing their outputs directly.

When I was in college for Mechanical Engineering, a teacher once said to me, "When you design how your final project is going to work, also design how you are going to test it."  Although this seems like simple advice,  I needed to make this mistake to fully understand the depth and importance of this advice.  This advice was told with mechanical systems in mind, but can and should be applied to programming practices as well. When we as programmers make a functions we know exactly what we expect out of it, but we just assume it works the way to planned, no debug errors, no problems. Since we know how to test the function why not make sure each function works as we program?  Function 1 leads to function 2 so make sure function 1's output is what you wanted before moving on to program function 2.  

This is probably obvious to a lot of programmers, but it was not obvious to me.  It took me making a 6 to 8 hour mistake to learn to test each function as they are created rather than after the full system is created,  but it was a lesson that I needed to learn.

Revamping the UI: Consider Events Handled

The final major step was to redo the in game level UI because it was filled with my programmer art.  At the same time, I found a way to make the entire code base for the system cleaner and more efficient.  The major component of these changes involved me learning about Events and delegates, something that I had been having trouble grasping the usefulness of until then. The first step was to add the art into the scene.  The scene's full transformation is shown below.

Before and after in game UI

This new layout included a few new things not in our original game design.  We added the ability to speed up or slow down the game, we limited the number of robots provided for each level and tracked this under the spawn robot button. and finally we added points to the game.

With the addition of all these changes it forced me to take another look at how the UI was being updated.  I originally had one script that stored all of these label and updated as other objects updated that main script.  This wouldn't work for the points because then each robot would need to reference this script for every instance that caused it to earn points.

This is where the EventHandler came in.  After some additional research I found one post that really helped me understand why and how I should be using events and delegates.  I would create an event for when a robot scored points, when a robot made it to the end, when a placeable was placed, and when a robot spawned.  These events are all stored in my EventHandler as shown below.

public class EventHandler : UnitySingleton<EventHandler> {

    public delegate void RobotGainedPoints(int points);
    public static event RobotGainedPoints OnRobotPoints;
    public static void CallRobotPoints(int points){
        if(OnRobotPoints != null){
            OnRobotPoints(points);
        }
    }

    public delegate void RobotCreated();
    public static event RobotCreated OnRobotCreated;
    public static void CallRobotCreated(){
        if(OnRobotCreated != null){
            OnRobotCreated();
        }
    }

    public delegate void RobotDestroyed(GameObject robot);
    public static event RobotDestroyed OnRobotDestroyed;
    public static void CallRobotDestroyed(GameObject robot){
        if(OnRobotDestroyed != null){
            OnRobotDestroyed(robot);
        }
    }

    public delegate void RobotScored(GameObject robot, int points);
    public static event RobotScored OnRobotScored;
    public static void CallRobotScored(GameObject robot, int points){
        if(OnRobotScored != null){
            OnRobotScored(robot, points);
        }
    }

The EventHandler stores the event, the delegate, and the function that can be called to trigger the event.  All of these are static so I can access them from anywhere.  This allowed me to easily have each label subscribe to their appropriate events and update whenever that event was call.  Thus, simplifying my code and cleaning up the MainCameraGUI script substantially.

This event system allowed me to track stats for our level completion screen that now appears after each level is complete.  Now that I learned how and where to implement events I feel my programming abilities have gotten that much better.

Revamping the UI: Select Your Level

The Level Select Screen required the largest amount of updating when we tackled the UI changes.  We went from a simple grid that placed all of our levels in one screen to a system that divides our levels into panels that can be swiped to reveal each other group.  The staggering change can be shown below.

BeforeAndAfterSpringItUI.png

Additionally, the panels could be changed by dragging the slider bars across the screen as shown in the picture below.  These slider bars would be stored on the right and left of the screen, but only two would be visible on each side at most.

Spring It Sliding across.png

You will notice, during the drag across, the red bar moved to the left and occupies the green bar's previous spot.  The yellow bar will then push the blue bar to the left as it snaps into its place on the left hand side. 

Creating this effect was the most challenging aspect of this screen and required me to learn a lot about NGUI.  Laying out the GameObjects in Unity's hierarchy was crucial to how this screen functioned.   The two pictures below highlight how the GameObjects were arranged for this functionality.

 Heirarchy

Heirarchy

The picture on the right shows how the slider bars were able to be moved across the screen.  Two scroll views were created, one on the left and one on the right.  These scroll views overlapped the main panel only slightly, the width of two slider bars.  This is the only section of the scroll panel that is visible to the player.  The rest of the scroll view stores the remaining slider bars to either the right or left of the panel depending on which side of the panel it is on.  Each slider bar's movement was constrained in the vertical direction but was treated as a DragAndDropObject for NGUI's sake.  Next, to prevent the slider bars from stopping mid drag, I made drop container's for each scroll view that occupied roughly half of the panel.  these drop containers take any DragAndDropObject that is dropped into them and stores them in an associated scrollview or table.

With that I was able to drag the slider bars across the screen, from one scroll view to another.  But before I was finished, I encounter one small issue.  The user could select the EITHER of the two revealed slider, thus messing up the order of my level panels.  To stop this, I created a click guard (just a collider) that would be placed over the slider bar I did not want clicked, thus stopping any click event from being passed to the slider bar below.  The next step was to hide or reveal each panel when the slider bar moved from one side of the screen to the other.  This was accomplished with the function below.

//From Class LSSliderBar
void UpdateAnchorPosition(){
  
        float percentOfPosition;

        mCurrentRelativeX = transform.localPosition.x + Mathf.Abs((int)mLeftGridPosition);
        percentOfPosition = mCurrentRelativeX/(Mathf.Abs((int)mLeftGridPosition) + Mathf.Abs((int)mRightGridPosition));

        mLevelGroupReveal.SetRevealPercentage(percentOfPosition);

        if(mLevelGroupHide !=null){
            mLevelGroupHide.SetHidePercentage(1 - percentOfPosition);
        }           
}
//From class LSLevelGroup
public void SetRevealPercentage(float currentRevealPercent){

        gameObject.GetComponent<UIPanel>().leftAnchor.absolute = (int)(mMaxPanelOpen * currentRevealPercent) + mStartingLeftAnchor;
    }

In the code, you can see that the slider bar checks its position relative to the total distance it can travel across the screen and then sends its percent of progress to the panel that is being revealed or hidden.  This panel's anchor is then changed accordingly.  The anchor on the panel must be changed rather than the texture behind it so I could achieve the sweeping effect.  If the texture in the background was parented to the panel and/or the texture's anchor was changed, the texture would scale down or up when the slider bar swipes across.  This was not what I was looking for and it does not look good for our game's aesthetic.

With the sliding effect finally working, I only needed to make sure the levels icons would spawn in on their correct panels.  That is where the LSLoadLevelGroup script comes in.

public void LoadLevelGroup(int currentSuffix, int endSuffix){

        Transform gridTransform = levelGrid.transform;
        levelGrid.columns = mNumberOfColumns;
        
        int currentLevelSuffix = currentSuffix;
        string levelName;

        for(int i = currentLevelSuffix; i <= endSuffix;i++){

            levelName = this.levelPrefix + i.ToString();
            GameObject newLevelItem = GameObject.Instantiate(this.levelItemPrefab, Vector3.zero, Quaternion.identity) as GameObject;
            newLevelItem.transform.parent = gridTransform;
            newLevelItem.transform.localScale = Vector3.one;
            newLevelItem.GetComponent<UILabel>().text = i.ToString();
            newLevelItem.GetComponentInChildren<UITexture>().mainTexture = mMenuLevelItemTexture;
            
            MenuLevelItem menuScript = newLevelItem.GetComponent<MenuLevelItem>();

            menuScript.levelToLoad = levelName;
            menuScript.levelLoadInt = i;
            LevelLoader.instance.AddLevel(levelName);
        }
  
        this.levelGrid.Reposition();
        mLSLevelGroup.StartLevelGroup();
    }

LSLoadLevelGroup is a relatively simple script that accepts two integers, the start and end suffix of the JSONs it will load, and outputs a table with 10 level icons.  I have one other script, LevelSelectLoader, that would distribute these integers to each of the level groups.  With these two scripts, the levels loaded in perfectly and we have a variable system ready for when we push past the current 40 levels.

The video below shows off the final product of this code. The screen is not complete yet, but is functionally close to how we envision our final product.

Revamping the UI: Main Menu

This post has been a long time coming and is part of a three part series where I will show all of the changes to our UI system.  After all of our UI changes to the main menu, the current setup is shown below.  A lot of these changes were purely art and design placement, but this post will focus on the programming required for the options menu icon shown in the bottom right corner.

FinalMainMenu

The options menu, when clicked, will first expand horizontally, then vertically to a set size.  This creates an expansion opening effect that looks great.  The fully expanded menu is shown in the image below.

Post Changes comparison.png

To create this effect I first started by laying out exactly what I needed to happen to make this effect.  

- Hide the Options Icon because that cannot be on the panel that expands. 

- Expand horizontally to a LerpTarget.  

- Expand Vertically to a LerpTarget

- Reveal the buttons.

Naturally, to close the same menu these step must be performed in the reverse order.

To Hide the Options Icon, I decided it would be easiest to just have another panel remains hidden until it is time to open.  At that point, the options panel would hide and this new panel would reveal itself.  The buttons, which also start hidden, were parented to this panel so their position would be unaffected by the panel's final expansion size.

Expanding the menu horizontally and vertically were both accomplished in the same manner, by Lerping the anchors of the NGUI widgets from a starting point to  the LerpTargets.  This proved relatively easy when opening the menu.  I would check at the end of each frame to see if the horizontal targets had been reached before continuing onto the vertical targets.  This is shown in the following code.

void LerpHorizontalLeft(){
        float lerpFraction;
        mCurrentLerpTime += Time.deltaTime;

        if(CheckIfHorizontalStarted()){
            mCurrentLerpTime = 0.01f;
            mCurrentLerpTime += Time.deltaTime;
        }

        lerpFraction = mCurrentLerpTime / mHorizontalExpandTime;
        if(lerpFraction >= 1.01f) lerpFraction = 1.01f;
        mMenuObject.GetComponent<UISprite>().leftAnchor.absolute = (int)((mLerpHorizontalTarget - mLerpHorizontalStart) * lerpFraction + mLerpHorizontalStart);

        LerpTransition();
    }

 

When I started trying to close the menu, I struggled to get the menu to close horizontally after the vertical had completed because the vertical lerp function did not have a way to transition back to the horizontal.  I made both the horizontal and vertical call back to each other if they both were not at their targets, but this was messy code that was quickly replaced when I encounter the next issue.

The menu button's position on the screen was not static for all scenes, so if I wanted to use the same code I needed to make it so the code could expand to the right or left and up or down.  I decided to standardize the design for the game my making the menu always open horizontally before vertically, and always close vertically before horizontally.  So to solve this problem, I created the LerpTransition() function that you can see in the code below.

void LerpTransition(){
        bool rightAtTarget = mMenuObject.GetComponent<UISprite>().rightAnchor.absolute >= mLerpHorizontalTarget;
        bool leftAtTarget = mMenuObject.GetComponent<UISprite>().leftAnchor.absolute <= mLerpHorizontalTarget;
        bool topAtTarget = mMenuObject.GetComponent<UISprite>().topAnchor.absolute >= mLerpVerticalTarget;
        bool botAtTarget = mMenuObject.GetComponent<UISprite>().bottomAnchor.absolute <= mLerpVerticalTarget;

        if(mIsOpening){
            if(mLerpHorizontalAnchor == "right"){
                if(rightAtTarget){
                    if(mLerpVerticalAnchor == "top"){
                        if(topAtTarget){
                            EndLerp();
                        }else{
                            LerpVertical();
                        }
                    }else if(mLerpVerticalAnchor == "bottom"){
                        if(botAtTarget){
                            EndLerp();
                        }else{
                            LerpVertical();
                        }
                    }
                }else{
                    return;
                }

The above code is a fourth of the entire function but shows us everything we need to see regarding this function.  I can set mLerpHorizontalAnchor and mLerpVerticalAnchor to the anchor I wish to move.  Then this function will go through checks to determine what step is next based on what steps have been completed.   This allowed me to greatly reduce the size of each lerp function.  I created 4 functions that lerp in the direction and mode I required: LerpHorizontalLeft(), LerpHorizontalRight(), LerpVerticalTop(), LerpVerticalBottom().  Each of these four functions refer back to LerpTransition so the program knows where to go next.  With the addition of this function the flexibility of the MenuOpenClose program increased greatly.  The additional control allowed is best highlighted through the variables shown below.

public string mLerpHorizontalAnchor;
public string mLerpVerticalAnchor;
public float mHorizontalExpandTime;
public float mVerticalExpandTime;
public Vector2 mFinalExpansionDistance;
public Vector2 mStartingAnchorDistance;

As you can see, the script allows me to set the start and end sizes of the menu with the mFinalexpansionDistance and mStartingDistance variables.  I am able to set the amount of time each expansion direction takes.  Finally, using mLerpHorizontal allows me to choose whether the menu will expand to the right or left, while using mLerpVertical allows me to choose whether it will expand up or down.

This program's difficulty rested in the organization and logical order of the code rather than the more typical math based challenges of coding.  I plan to use this code's functionality for many different applications and future games.

The full code is displayed here.

Post Playtesting

Although all of these post are about programming and design, I also do the project management and some of the leadership type roles for Spring It.  Now that our first major playtest session has completed I decided to write this post about both of these roles.  We received a lot of feedback from friends regarding the game.  We had most people fill out a google form and the results were compiled into an excel sheet for us to look at.  A few of our friends wrote out their experiences in paragraph form in the style of a play by play.  We had largely positive feedback with the major complaints being bugs, a desire to know performance at the end of a level, and more motivation to play each level. 

At this point, we took a step back to revamp our overall design.  Our GDD was desperately in need of an update because our design had changed so much from our original.  We knew we had a good grasp of how each level would play and where the fun was.  We needed to more effectively hone in on this fun as well as create the over aching system of how all the levels will progress and connect to each other.  We knew we needed a summary screen, and we needed each level to unlock the next.  We also needed each "block" of levels to unlock the next block.  But we had not really decided how the player could lose at our game.  Being a puzzle game, losing just means you have to try the puzzle again by restarting the puzzle, but levels already have a built in reset system.  The player places tools, starts the assembly line, and tweaks it as robots progress down the line, when they need to make major changes, the reset the scene and stop the robots.  After brainstorming many ideas, we settled on changing the system so one robot spawns at a time, and the player only receives a set number of robots before the entire level resets completely, this makes each robot worth more to the player and has them thinking about their placement before starting the assembly line.

Great, we have a way for players to lose.  Next we needed to determine how each "block" or levels is unlocked and how many levels a block contains.  We wanted the first block to introduce the core mechanics that we have in our playtest build.  We settled on allowing the player to choose from two new mechanic blocks, bumpers and magnets.  When they unlock one, the other one locks and then requires more "stars", or whatever we decide to use, than the original unlock.  After the player completes both mechanic's blocks,  a block combining the two new mechanics.  This structure would be our goal for the remainder of the game.

After hashing all of these details out, we finally had a clear scope for the remainder of the game.  up until now there had been a lot of changes to mechanics and even more  changes to the art style.  To spare us the wasted work, we wanted a unified agreement from everyone for the final art style.  Before we could start sprinting again, I needed to rewrite our entire product backlog from scratch.  To assist me in this task, I rewrote the GDD with all of our updates to give me a clear idea of everything we still needed..  Next, I set to writing the new product backlog.  Since I still have incomplete information about how long the art will take, I only filled in the art tasks I knew of and left  " Define New Art Style" as our highest priority item.  Previously, the backlog only had priority and task name, but because we finally gave ourselves a time limit for this game (End of the Year), I added a third column rating the time each task will take.  This means I can finally predict what we will have time for.  So with these changes, we restarted our sprinting last week.  As an added benefit our game is a more modular system which we can add new mechanics onto freely.

We will be watching how our new changes feel to the designer, the scope of our art style, and I will be pushing to plan for our next playtest. (Sorry for the lack of pictures so here is a screen shot of our newest sprint and product backlog)

More Player Feedback on Placeables

When players were placing the objects from the UI, the object would not always spawn where the icon was, leaving the player confused.  To solve this issue, we decided to spawn the object into the scene the moment the icon is no longer above the UI.  Additionally, we wanted to make it clear to the player that the object they are dragging is not in the scene.  To convey this we wanted dragged objects to be transparent.  Finally, to let players know when they were putting the object in an illegal spot, say on a Spawner, Furnace, or outside the level, we would highlight the object red.  The two videos below show our desired effect.

To tackle this problem I took a look at what systems were needed code wise.  

- A system to turn objects transparent

- A system  to turn objects red. 

- A system to check for invalid spaces: Floors, Furnaces, Spawners, Collectors

- A system to check when the object is outside the level.

Because turning objects red and making them transparent would require similar functionality, I decided to make them into one class.  I started by making a class that would change the color and/or transparency of the Placeable.  I quickly found out that our current shader does not support transparency.  I talked to our artist, our shader will not be changing, so that option was out.  Okay, so I needed to pick a transparent shader for my code to store. I choose "Transparent/VertexLit" because it was the least detailed.  I stored our original shader as well so I can easily switch between the two.  All I needed to do was loop through each object with at least one material and change all their materials to transparent.  This same method was applied to a change color function when changing the object to red.  Finally, just for readability's sake, I made the ChangeShader function private and two public functions that could be called to make the object transparent or opaque. 

The final code is shown below.

public void MakeTransparent(){ChangeShader(mTransparentShader, false);}
    public void MakeOpaque(){ChangeShader(mNormalShader, true);}

    void ChangeShader(Shader incomingShader, bool state){
        for(int j = 0; j <mAllChildrenWithMaterials.Length;j++){
            for(int i = 0; i < mAllChildrenWithMaterials[j].renderer.materials.Length; i++){
                mAllChildrenWithMaterials[j].renderer.materials[i].shader = incomingShader;
                mAllChildrenWithMaterials[j].renderer.materials[i].color = mTransparentColor;
                ToggleColliders(state);
            }
        }
        
    }

    public void ChangeColor(){
        for(int j = 0; j <mAllChildrenWithMaterials.Length;j++){
            for(int i = 0; i < mAllChildrenWithMaterials[j].renderer.materials.Length; i++){
                mAllChildrenWithMaterials[j].renderer.materials[i].color = mInvalidSpotColor;
            }
        }
    }

Once I had the Placeable turning transparent when it moved,  I needed the next class, ValidPlaceablePosition(), to check if the object is in a valid spot.  I determined the easiest way to do this was to check colliders. The first problem I ran into with this approach was that the colliders would trigger on EVERYTHING.  So to fix this, I made two new layers and made them only collide with each other.  One layer for everything the Placeable can check and one to indicate which placeables were invalid spots.  Once I made this change, the OnTriggerEnter and OnTriggerExit functions would not be called nearly as often.  

My next revelation came when I realized I could add a collider to all of the panels in the Foreground.  This allowed the Placeable to trigger when it was outside the level bounds (aka covered by the foreground)  and as a result, can be marked as being invalid.  Now that I had everything triggering correctly,  I needed a way to make sure I was checking the correct collider.  Another problem arose when the Placeable would be placed on a floor with a furnace, three colliders would be entered at once.  So I needed to make sure I was storing only the most relevant collider.  Once it was stored, I needed to make sure this collider was not removed until that collider was actually exited. With a series of if statements, I could easily make sure only to apply a new collider if the new collider is an invalid position.  This code shown below.

    void OnTriggerExit(Collider incomingCollider){
        if(mMainBodyCollidingObject == incomingCollider){
            mMainBodyCollidingObject = null;
        }

        IsValidPosition();
    }
    
    void OnTriggerEnter(Collider incomingCollider){
        if(mMainBodyCollidingObject != null){
            if(incomingCollider.gameObject.layer == mPlaceableHazardLayerInt){
                mMainBodyCollidingObject = incomingCollider;
            }
        }else{
            mMainBodyCollidingObject = incomingCollider;
        }

        IsValidPosition();
    }
    
    public bool IsValidPosition(){
        
        mIsValidPosition = true;
        IsThisAnIllegalBackgroundSpace();
        
        return mIsValidPosition;
    }
    
    void IsThisAnIllegalBackgroundSpace(){
        if(mMainBodyCollidingObject != null){
            if(HasAnIllegalTag(mMainBodyCollidingObject.tag)){
                mIsValidPosition = false;
            }
        }
    }
    
    bool HasAnIllegalTag(string incomingTag){
        for(int i = 0; i < mIllegalObjectsTagList.Length;i++){
            if(incomingTag == mIllegalObjectsTagList[i]){
                return true;
            }
        }
        
        return false;
    }

The final result works well as you can see in the videos above.  The hardest tasks was just laying out the system and how to check if I was outside the level.  My mind became stuck, as it sometimes does, on the idea that I would just make sure the position of the peaceable was within the level bounds.    This was not an effective approach but luckily I found a much easier solution.  Feel free to ask any questions or if you would like to to elaborate further on any of this task.  

Parasite Style Support

So this week I decided to flex my design skills.  I did some research into a game I play, League of Legends, and created a Champion.  I will talk about my design decisions to the best of my ability.  Obviously everyone of these abilities will change in playtesting but the overall themes and interactions would hopefully remain.  Numbers were added to help give a comparison of this champion to other champions, but are only included in the Google doc version.

The full Game Design Document can be found here.  For this Post I will talk about each of the abilities individually and I will talk about my design decisions behind each ability.  If you enjoy looking at numbers and balancing, I have a spreadsheet that shows a lot of my work here.

Disclaimer:  I give Riot permission to use any/all of these ideas, they are just ideas, implementation is what is will make this champion fun if at all.


Passive: Adaptation

This champion’s stats change based on other champion distance to him.  If an ally is nearby he gains armor and magic resist based on the distance they are from him.  If an enemy is close to this champion to him he gains additional AD and AP.

Additional Notes:

If this champion and an ally/enemy occupy the same space this champion receives the maximum benefit.  The maximum distance this benefit is received is slightly larger than Evelynn’s invisibility circle: 750 approximately

 Source:http://i.imgur.com/Pig8E.jpg

Source:http://i.imgur.com/Pig8E.jpg

Design Choice:

My intent with this champion was to be a defensive support.  Slow the enemy, block hits, and stay alive.  This was to provide additional damage reduction and create threat when charging and attaching to an enemy.  The defensive benefit will be small unless this champion stays close to their adc.


Q: Weaken

This champion places a dot on target enemy champion that deals minor magic damage every 2.5 seconds for 5 seconds.  Every time an ally deals damage to this enemy champion with a basic attack, the enemy champion will be dealt the magic damage again.

Every time the target is dealt damage by this ability, this Champion gains armor and magic resist for 6 seconds and the target loses 2 armor and 2 magic resist for 6 seconds.  This armor and magic resist transfer stacks and its duration resets each time damage is dealt.

Find Host Changes:

    If the Host is an enemy, this ability is applied as a continuous debuff to the host and this ability can be used on an additional target.

    If the Host is an ally, this ability changes to:

    Infuse

When activated, the host’s next auto attack will apply this ability to its target. The host and this champion both receive the armor and magic resist benefit.

Design Choices:

I wanted an ability that could foster teamwork, increase tankiness, and maybe be harassment in limited use.  The main goal was to foster teamwork, so having the damage dealt by their ally’s contribution to their harrass was ideal.  The stealing of Armor and Magic resist gives this ability additional threat and fits the theme of a parasite.  

The Range was chosen to give this champion one ability they can use without guaranteed to be harassed.  This champion will be more about the counter engage so this using Weaken will most likely cause this champion to be attacked if it is not used at the right time.

When testing this champion I would watch the Damage, armor gain, and magic resist throughout their scaling in the game.  I would also keep an eye on how often this debuff is triggered in team fights (It will probably be too much). This is not the main portion of this champion’s kit and should take a back seat to changes on other abilities.


W: Feed

This champion deals magic damage to all surrounding enemy minions and champions.  This champion heals for a percentage of his health based on the number of minions and champions hit. This ability heals more for each champion hit than for each minion hit.

Find Host Changes:

If the Host is an enemy, this range is increased to 550.

If the Host is an ally, this ability changes to:

Nourish:

This champion sacrifices a percentage of its health to restore health to the host.

Design Choices:

This ability is here to give this champion survivability in lane.  The mana cost was determined by looking at other healing abilities and was eventually settled that nunu’s most closely resembled what this did.  Unlike triumphant roar, I do not believe the mana cost needs to change with rank because its use should not be limited by available mana in later stages of the game.

Damage was set to have lower base amount because it is AOE damage.  The damage was chosen to scale off of maximum health after looking at Braum’s damage on winter’s bite.  The damage portion of this ability is minimal to the effect of the champion but, if it hits all five enemies it can become a problem

The Range was based off of  Amumu’s Despair.  It feels like a good range for this ability.  This champion must get close to the enemy to gain back life, but the threat of paralyzing Injection should make these skirmishes more successful.

When deciding what cooldown to use, I looked at triumphant roar, imbue and consume.  Both Triumphant roar and imbue can heal allies and can have their cooldowns reduced.  I believe this will heal less than all three abilities early on but as the game progresses, it will surpass them.  So giving this ability a static cooldown ensures it can be used more often than all three early buffing its early game and curving its scaling.

At first I had the Host benefit be a reduced cooldown, but upon further thought, I feel this ability would be easily shut down if the player just moves away from nearby minions, so increasing the range this affects looks like a more interesting choice.  It forces better player interaction than just reducing the cooldown. Amumu’s ultimate was used as a template.

Nourish is limited to be always equal to or less than the amount of health sacrificed because this champion’s health should be worth less than the host’s.

I would watch the Range, the Cooldown, and the heal amount when playtesting this ability in that order.


E: Paralyzing Injection

This champion fires a short range skillshot.  Any enemy champion hit by this injection has their movement speed reduced for 3 seconds.  This ability stacks until the total debuff would reduce the target’s movement speed by 100%.  

Find Host Changes:

If the Host is an enemy, this ability also reduces the attack speed of the champion by the same amount their movement speed is reduced by.  If this ability reduces the champion’s attack speed by 100% they are instead stunned. This allows the host to become stunlocked if left alone for too long.  

            If the Host is an ally, this ability changes to:

            Absorption:

This champion absorbs all negative effects off of the host.  If an absorbed effect causes this champion to be stunned, he is immediately detached from the host.

Design Choices:  

I wanted a CC ability for this champion but also something that fit the flavor of a parasite inhibiting their host.  At first I had this ability reduce movement speed AND attack speed by 33% but its cooldown was so long it would not be able to stack.  While this champion was attached, they would be able to make it stack rather quickly.  This made the ability seem useless to me outside of being attached.

To solve this problem I reduced the cooldown of the attack so it can normally stack if used correctly.  This gave the main attack meaning in my mind.  Now the main attack was too strong seeing as it could stun lock a champion with relative ease.  I separated the attack speed slow to only exist on the ultimate to give the ultimate more power with respect to the base attack.  I feel this ability now feels slightly like cassiopeia’s noxious bite but for utility purposes.  

When playtesting this ability I would watch the mana cost first and foremost, then the cooldown with respect to the duration, and finally keeping an eye on the range.  If this ability feels like it can be activated to early, then this champion should be able to catch nearly all champions.


R: Find Host!

Mana: 120

Cooldown: 90/75/60

Range: 1000

Radius: 175

Duration: 8/10/12

    This champion launches itself in a toward a targeted area of radius 175.  This champion will attach to the champion closest to the center of the targeting circle.  If there are no champions present in the targeted area, this cooldown is reduced by half.  When this Champion hits another champion, that champion becomes the host.

While attached to a Host, this champion cannot move.  This champion will stay with the host through all speed boosts, jumps, Invisibility,  Teleport, and Flash.  They will not turn invisible with the champion that goes invisible.  For specific Champion ability interactions, please consult the list in the additional notes below.

This Champion can only stay attached to a single target for 6/7/8 seconds, this ultimate can be reactivated while attached a Host to jump to another targeted area of radius 175.  If the ability hits another champion, this champion will attach to the new champion, after a one 1 second delay. This can only be used once to jump between Hosts.  If the ult is activated again, this champion will jump away 450 spaces from the side they were attached to and detach from the Host, ending the ultimate.

- If host champion is an ally, this champion attaches to their back.  All skillshot and damage coming at the host’s back is instead hits this champion

- If host champion is an enemy, this champion attaches to their face.

 

While attached this champion  will not be affected by:

Blinds, Entangles, Forced Actions, Slows, Roots, Any Cleanse, Any Knock Asides, Some Knockups, and Some Knockbacks

This champion can be removed from their host after:

End of the Ultimate, Any stun, All Flings/Grabs except Diana’s Moonfall and Darius’s Apprehend,  Any knockback attack that displaces this champion by 300 units or more, Any Suppression, Any Polymorph, this Champion’s Death, and Death of the Host.

Design Choices:

This ability is the major component of this champion.  I wanted a champion that would mimic the movement of another and I designed the kit around this attachment ability.  This also helped me decide to target this champion more toward the support role because I feel this ability would be too hard to balance in any non team fighting capacity.  Bottom lane naturally experiences the most of this, and that reinforced my choice.

The hardest choice with this ability is how it can be cancelled/ended.  Right now I believe there is sufficient immunities and removals as a starting point for this ultimate, much will probably change in playtest. I went with knockbacks like Alistar’s headbutt because I felt it made sense that a large bull charging you would knock this champion off their perch.  The reason I excluded airborne knockups was because I feel it is too difficult to limit detachment by timing of how long the knockup would last (1 second or greater).  If all abilities that knocked up the champion would detach them, miniscule abilities would end the ult.  Examples would be, Blitzcrank’s power fist, nami’s wave, riven’s final jump.  These all felt inappropriate to end this ultimate.

At first I had this ability focus on only one champion, but the amount of time the ultimate would last felt too short for what this champion would do.  I expected this champion to lock down a character, if left alone for too long, with paralyzing injection.  If the ult lasted too long, this champion felt overpowered (Concept wise), if the ultimate was too short (6 seconds) the Host would only be stunned for 2 or 3 seconds at the end of the ultimate, which did not feel strong enough.  What ultimately helped me decide to change the ultimate was the fact that once the player found the host, they no longer had any interesting decisions to be made.

I finally decided on the ability to jump between two champions to allow more decisions for the player.  Before, the player’s only choice was, jump to enemy, use all my abilities quickly, detach when it feels right.  Which that change, the player has to choose if they want to use each ability on this host or their next.  They also have another chance to use it as an escape by jumping between hosts to avoid stuns and other abilities that would detach this champion.

 

Walls. THE WALLS!

Once we had the level framed and the textures were mostly lining up, I needed to cover the smaller areas where you could still see the blue background.  This is shown in the following picture.

Level Befoe Walls.png

So to accomplish this, I needed to know where each tile ended and started at the floor.  From there I could figure out where I needed to cover with walls.  So the first script I was called every time a texture was made by the BuildFloor script and added its transform to a List.  Then once the program ran, it would get the upper right and upper left points of Floor Tiles.

public void NewWallLocation(Transform incomingTransform){

mWallTransforms.Add(incomingTransform);
}

void SetWallPoints(){

for(int i = 0; i < mWallTransforms.Count; i++){

Vector3 tempPosition = Vector3.one;
tempPosition.x = mWallTransforms[i].position.x -mWallTransforms[i].localScale.x * mStandardTileSideLength;
tempPosition.y = mWallTransforms[i].position.y + mWallTransforms[i].localScale.z * mStandardTileSideLength;
tempPosition.z = mZDistance;

mWallPoints.Add (tempPosition);

tempPosition.x = mWallTransforms[i].position.x + mWallTransforms[i].localScale.x * mStandardTileSideLength;
tempPosition.y = (mWallTransforms[i].position.y + mWallTransforms[i].localScale.z * mStandardTileSideLength);
tempPosition.z = mZDistance;

mWallPoints.Add (tempPosition);

}
}

From this, I wrote out all the cases that the program might run into to: 

- The startPosition could be above the endPosition and they could have the same x value.  This means the wall points are directly above each other and you need to build a wall from start to end facing to the right of the level.

-  The start Position could be below the end Position and they could have the same x value.  This means the wall points are directly above each other just like the previous build but the wall needs to face to the left instead of the right.

The next cases all brought up problems because they involved the a situation where there was a hole in the floor.  In all three hole cases, they do not have the same x values. But their startPosition could be higher, lower, or the same height as the endPosition.  This is easy to spot with respect to the previous 2 examples and simple to implement.  I only need to spawn walls fromt he startPosition and the end Position to the mLowestFloorPosition(-13 in unity space in our case).  One will face right and the other will face left. The problem arises with the sixth case.

The sixth case is when both ends of the same texture are selected.  This looks exactly like a hole but it is different in that walls should not be spawned at all.  To solve this problem, I realized that the only time walls would ever spawn was on odd points in the array, so between points 1 and 2 between 3 and 4 and so on.  The texture issue would only arise on even numbers of the array, 0 to 1.  This is because the wall points were added in pairs, each texture has two corners.  So I only needed to check every other corner.

Before implementing this though, our level designer did something cool; he just placed floors two spaces lower than the main floor to make a hole where he wanted.  I think this just looks better aesthetically and from a  design perspective, it allows the player to better see that their robot will not be moving anymore. So as a result, I did not have to implement the hole option into the code.

The Final result does not have our artistically pleasing wall texture but it does not look bad and can be seen below.

Level 1 walls.png

Framing the Level

At GDC we noticed the game was visually lagging when you moved across the screen so, this week's task was to fix that issue.  We were looking to reduce the number of models in our scenes and create a better design for the foreground.  Originally, I had the Unpack Level script instantiate floor cubes below each of the lowest floor pieces until they reached out of sight of the camera. This resulted in a lot of models being created at run time which unnecessarily slows down our game during play mode. So I wrote a program that would find the positions of the lowest floors and create a texture(s) below all of them.  You can see the result in the picture below.

To start with this script, I wanted needed it to know what floor was the lowest in each column.  So I wrote a simple series of for loops that take the DataLocations from Unpack Level and populate an array.

//Go through each column(mLevelSize.x) and findthe lowest place a 17(Floor) appears
//The values fill horizontally so for a level 10 long you need to check data point 0, 10, 20 ,30 ,40...
void FindLowestFloors(List<int> mDataLocations){

int currentLowestFloorDataPoint = 0;
for(int i = 0; i < CameraMovement.mLevelSize.x/2; i++){
for(int j = 0; j < mDataLocations.Count; j = j + (int)CameraMovement.mLevelSize.x/2){

if(mDataLocations[i + j] == 17){
currentLowestFloorDataPoint = i + j;
}
}
mLowestFloors[i] = ConvertToPosition((currentLowestFloorDataPoint));
}
}

Once I had the list of lowest floors, I searched through each one to check if they were higher or lower or at the same height as the current floor.  Once I found a floor that was higher or lower, I would stop the search and run my function CreateFloorTile.  This function, shown below, creates a Plane in Unity, changes the Scale and Position so the texture will cover all of the area underneath the floors, then adds a new material.  All of this is not done in just one function, that would be horribly hard to read.

void CreateFloorTile(Vector3 startPosition, Vector3 endPosition){

GameObject newTile = CreatePlaneObject();

newTile.transform.localPosition = SetTilePosition(startPosition, endPosition);

newTile.transform.localScale = SetTileScale(startPosition, endPosition);

Vector2 textureScale = new Vector2(1,1);
textureScale.x = newTile.transform.localScale.x;
textureScale.y = newTile.transform.localScale.z;

SetupShaderAndMaterial(newTile.renderer.material, textureScale);

this.gameObject.GetComponent<BuildWall>().NewWallLocation(newTile.transform);
}

Unity does something dumb (and so do I)

Once I had all of this working, I had textures spawning in the correct place under each of my floor sections. The textures were also scaling to the lengths of the tile and they all looked sorta good.  The problem was, the tiles were not lining up. So after playing around in scene view I wrote down where all of the textures needed to be based on their scale and offset.  I knew the offset moved the texture to the right.  So I tried to use math to find the equation that would allow me to determine what the offset should be so all of the textures looks like one seamless texture.  After 2ish hours or working on it and a day or two to let it stir in my brain at work, I thought, " What if my assumption that the scale will fill by adding the new parts on the right and is fixed on the left, was wrong?"  So I made a test texture, which you can see below, to test this theory.

It turns out, even though the offset moves the texture to the right, the scale will fill in new section on the left.  I definitely need to watch my assumptions from now on because that cost me  a fair amount of time, but in my opinion, Unity's texture system is not intuitive and that was frustrating.  With that new knowledge, I was easily able to create a system that tracked the previous tile's offset and allowed me to line all of the textures up again so they for one beautiful connected texture.


I applied these same techniques to create the three panels that surround the scene that you saw in the first picture of this post.  Most of the time was spent creating readable code and doing tweaks that would make sure the textures on the side and top panel would line up nicely with the previously created floor.

Spring It's New Camera Movement

It has been a busy past two weeks so I missed posting last week.  Last Friday I started working on filling out the areas around our spring it levels.  When I started doing this, I realized that once the camera was able to zoom, the area that I need to cover would have to be changed as well.

Camera Movement.jpg

In the diagram above, the Full size of the level is represented by the black box on the left of the picture.  The Green tetrahedron-like object represents the camera's view screen when fully zoomed out.  So, to make sure the camera cannot move in the X or Y directions, I would just have simple if statements check those bounds.  The problem would then arise when the camera would zoom in.  The green camera view port would move along the red line labeled 1 until it was the size of the blue camera view port.  If the same limits that applied to the blue camera also applied to the green camera, the player would not be able to see anything with the blue camera.  It was clear I needed to make the camera movement limits dependent upon how far zoomed in or out the player was.


  After taking a look at the varying level sizes, I decided that the camera's maximum zoom distance will depend on the level.  Meaning the camera will have to stop once it can see the entire height or width do the scene, whatever comes first.  So, I started by writing a function in the level loader to determine the max zoom distance. So after some basic algebra to determine the equations of the lines and their intersection points, I ended up with the following code.

//The Camera Zooming Out needs to be limited to only showing the entire level length or height in one view (Thsi will determien which one will be shown first and will set the limit at that)
float SetCameraZoomOutMax(){

//The limits of the camera's movement equation was determined in two parts. The first part is from (5.5, y, -10) to (9.5, y, -20).
//After -20 its movtion follows a different equation based off (9.5, y, -20) and (24, y, -37).
//Where this line form each side of the level meets means there is the x limit
float zoomOutLimitForWidth = (-17f/29f) * ((int)mLevelWidth * (int)mLevelScaling + 15.12f);

//Both the top and bottom have different equations and where those two equations intersect will determine where your backward limit is
//These equations are the equations that dictate movement
float zoomOutLimitForHeight = (-7f/13f * (((int)mLevelHeight * (int)mLevelScaling) + 30.14f));

if(zoomOutLimitForWidth > zoomOutLimitForHeight){
return zoomOutLimitForWidth;
}
else{
return zoomOutLimitForHeight;
}

}

Using the same equations I developed to determine the Max Distance the camera can zoom out, I was able to set limits on the camera's movement depending on how far the player was zoomed in. the problem I found was that two equations were needed:  One equation would get the player from their minimum zoom in distance to the camera's start position at z = -20, the second equation would dictate movement from the start position to their furthest point.  The reason was that once the camera was further out, the slope of the limits steepened.  This process most likely could have been done with one non linear equation, but this method is easier on me and accomplishes what I wanted.

In the code below:

LW = Level Width

LH = Level Height

float CalculateVariableCameraMoveZoneLimits(string incomingDirection) {

//This is the x or y limit depending on what case is encountered
float currentLimit = mMouseMoveZone;
float zPosition = this.transform.position.z;

switch(incomingDirection){


default: break;


case "right":
if(zPosition > -20){
//Xright close: LW + 4/10 * z - 1.5 = y
currentLimit = mLevelSize.x + 4f/10f * zPosition - 1.5f;
}
else{
//Xright far: LW + 14.5/17 * z + 7.5 = y
currentLimit = mLevelSize.x + 14.5f/17f * zPosition + 7.5f;
}
break;

case "left":
if(zPosition > -20){
//Xleft close equation -4/10 * z + 1.5 = y
currentLimit = -4f/10f * zPosition + 1.5f;
}
else{
//Xleft far: -14.5/17 * z - 7.5
currentLimit = -14.5f/17f * zPosition - 7.5f;
}
break;

case "down":
if(zPosition > -20){
//Ylower close: -7/10*z = y
currentLimit = -7f/10f * zPosition;
}
else{
//Ylower far: -6/7*z - 3.14 = y
currentLimit = -6f/7f * zPosition -3.14f;
}
break;
case "up":
if(zPosition > -20){
//Yupper: -LH/10 * z + 7 - LH = y
currentLimit = (-mLevelSize.y * 10f / zPosition) + 7;
}
else{
//Yupper: z + LH + 27 = y
currentLimit = zPosition + mLevelSize.y + 27;
}
break;

}

return currentLimit;
}

The Camera system will still need more upgrades as the game progresses.  Notably, changing camera movement speed based on scroll distance.  This would cause the player to move slower when zoomed in and faster when zoomed out.  This would be relatively simple for me to implement, but because of upcoming GDC, it is not on our current sprint.

The second system I hope to implement is change how zooming out works when the player crosses a movement limit.  Currently the player can zoom in, move all the way to the left or right of the screen, then zoom out.  When they zoom out, they will go directly backwards, moving out of the limits I set for horizontal and vertical movement.  This then allows the player to see the ugly Unity blue around all levels. To fix this I plan to  put similar checks into the zooming out motion, but instead of preventing movement, the camera will just be moved horizontally or vertically and before zooming out.  Hopefully creating a smooth zooming motion for the player.

Our First Build

This post will be more design focused because we made this first build hoping to allow potential sound designers to play our game before making a sample track for us.

Here is our first Build. 

This was my first time assembling a build in unity. It was not as simple as clicking the build button  after the build settings have been determined and the levels correctly ordered. It turns out that NGUI has some specific requirements when it comes to fonts and effects.  So i had to hurriedly find a font and put it into the build.  The current one is not permanent, it hurts to read. Once the build was working, I needed to ensure the players would know how to play, so I made a controls screen as shown below.

Controls screen.png

It is not the most impressive screen, but it gets the job done (I hope).  Eventually we plan to make the game's first levels teach the player as they go.  Once this screen was complete, I needed to make sure we had levels that showed the games current features and gave an overall feel of the game.  Because of a change I made to the level editor, our previous levels did not work, so I needed to start from scratch.

I created a list of features and the order in which I wanted to introduce them.  Eventually, I decided on 10 levels:

Level 1 - Show how to place springs for the first time

Level 2- Show backwards conveyors

Level 3- Another level with just springs being placed

Level 4 - Show that springs can be placed in mid air but at a higher price

Level 5 - Allow the player to place conveyors

Level 6 -  show the player that the fan object exists

Level 7 - Let the player get used to the fan objects in a more complicated level

level 8 - Show the player the furnace object exists

Level 9 - more practice with the furnace

Level 10 - allow the player to place the fan

If I had more time (it was 3 am that night and I could not work on it the next day), I would have had more buffer levels between the placing of conveyors, introduction of the fan, and introduction of the furnace.  I also would have had more levels where the player can place fans because that is where the game will really start to shine.

 Level 5's Tiled view

Level 5's Tiled view

Again I am really happy with the Tiled Level editor code i wrote because it allowed me to push out these levels in 2 to 3 hours.  These are just some examples of the levels I made.

Level 9 Picture Musician's Build.png

Play testing Family Fortune

This is the first time I was able to take pictures of one of my play tests and the first play test I have held during the running of this website.

This game was played during the weekly board game night I host at my apartment.  My friends, Odin, Philip, Huong, Phil, and Kileean played this game with me for a total of 6 players.  This was the second time some of them had played the game but the game experienced a major change between these two games.

IMG_20140127_195408.jpg

The full rules for Family Fortune are available on this site, but I will give a quick overview here.  Family Fortune has the players searching a mansion for a $1,000,000 treasure hidden within, while grabbing any other valuables they come across.  Players will reveal the house as they rush through it.  They search rooms for items or clues to help them discover the $1,000,000 item.  At the end of the game, the player with the most money wins.

IMG_20140127_202915.jpg

The first time this group played this game the main complaint I received was there was not much variation in people's play styles.  They didn't feel like they needed to make choices during their turns.  One of my friends suggested I add special abilities to each of the characters.  So this iteration, all characters had a special ability. Some of the abilities made certain items more valuable while others changed how the character operated completely.

When I made certain characters I had ideas of how they would play:  their abilities reflect this.  Cousin Sally was suppose to be smart and easily able to figure out what item was worth the 1 million.  As a special ability I allowed her to freely look at the clue in any room she passed through.   This made Huong (my friend who played her) prioritize going to rooms with clues in them.

Overall, the players enjoyed the addition of the abilities because they felt they played differently based on who they were.  A couple abilities felt minor and are being watched in games to come.

IMG_20140127_201107.jpg

After playing this game and listening to a few comments, I have decided to make a few changes and found places to watch.

Mainly, a few character's special abilities felt weak. So, instead of Uncle Bill looking at 2 rooms whenever he reveals a new room, he will look at 3. He can put them back in any order, or even on the bottom of the deck.. This should allow him to always get a room with something he wants in it.  

Secondly, I am going to remove the upstairs completely.  It does not mechanically matter and only makes for a more cluttered table.


I was given the opportunity to play test this game a second time just last night with my friends Ben and Griffin.  This game could have been even more helpful in understanding the faults of the game then the one played with 6 players last Monday. 

The first thing that stood out to me was the trouble with ending the game early.  Whenever my friend Ben plays a game, he will end the game as fast as possible. He looted his first set of items and once he made it outside, he called the cops to signal the countdown toward the end of the game.  So seven full turns later, the game would end.  This made me feel the game was unnecessarily short and less enjoyable than the game on Monday.

Another issue we encounter was that both other players felt clues were not adding anything to the game and they completely ignored them.  This confused be because, previous plays told me that player very much enjoyed the clue system. With such a short game this time, not nearly enough clues appeared for any player to even have an idea of what the 1 million dollar item was.  So this entire aspect of the game was not experienced by the players at all.

This game made me realize I may need to change a few things for smaller groups of play.  The house is very large, and will not be fully explored in full groups.  As a result, not all items will be revealed to all players. I plan to play test more with 4 players specifically because that is a typical play group.

Additionally, to combat the short game game feeling, I may experiment with requiring all players to have an item in their car before the cops can be called.  This would allow everyone to have a voice in choosing when the game ends.

As I am able to try more play tests, I will be targeting groups of 4 people and implementing minor tweaks until I get the game right.  Once I feel the game is fun enough, I will begin drawing up the tiles in Gimp to create a better aesthetic to the game.  I will continue to update my thoughts of each play test as I host them.

Tiled Level Editor Details

This post will go into the details of how I created the Tiled Level Editor for the game Spring It.  Spring It is 2D and grid based.  Unity 3D does not have an easy way to build levels to a grid we specify and would often cause the pieces placed by the player to not align to the piece we placed during level building. So I wanted to make a level editor that ensures we are placing our level pieces to the same grid the player is placing theirs.  One of my team members, Chris Flynn, suggested I use Tiled map editor as a start for this level editor I was hoping to make.

Tiled Ezample Full Shot.png

Tiled can export to JSON but Unity 3D does not have a built in JSON parser like it does for xml, so I decided to use MiniJSON by Calvin Rien.  Using MiniJSON proved to be very helpful and made the parsing much simpler.  First I created a string of names for the tiles so I knew what I was reading when looking through each of the layer's data sets.  Due to the way the data was stored and how I wanted to access it I needed to store each set of data to their own array and they each needed to be parsed in their own unique for loop.  Once they were all stored I could access each of them to determine what tiles existed in each level, then check with my tile names to pinpoint what object was where in the grid.  For example, if I read a number of 1 or 2, I know a conveyor is placed. The difference is that a 2, means the conveyor moves the robots right, while a 1 conveyor moves the robots left. 

Conveyor.png

Once all the data sets were in their appropriate spaces and the levels were built, I still needed to construct the nuts and bolts of the scene.  The Main Camera, Background, etc still needed to be put into the scene.  On top of that, the variables that were unique to each level still needed to be constructed.  This is where the Budget, AvailablePlaceable and RobotDataLocations layers came in handy as they were used to keep track of the correct money usable by the player, enabled objects the player can place, robots spawned, and robots needed to win.  This section of code just took a large amount of breaking down into smaller functions to help me keep track of everything that was happening. The code below is the end result of my work and will be updated as the project evolves. When using the code, just place the exported JSON document into the Text Assest mLevelFile.

//Below is an example of what you should get from the tiled level editor when exported
//The first set should be 8 layers similiar to what is shown below
//{ "height":15,
//"layers":[
// {
// "data":[0,0,...]
//"height":15,
//"name":"Level Layer",
//"opacity":1,
//"type":"tilelayer",
//"visible":true,
//"width":20,
//"x":0,
//"y":0
//},
//
//Below this point shows what each fo the tile sets will look like, there should be 8 of them in total as well.
//The difference with them is that their identifying attribute "firstgrid" will nto increase by 1 for each tile set becasue many of the sets have more than 1 tile.
//"tileheight":256,
//"tilesets":[
// {
// "firstgid":1,
// "image":"Tilesets\/Conveyor\/Conveyor.png",
// "imageheight":256,
// "imagewidth":512,
// "margin":0,
// "name":"Conveyor",
// "properties":
// {
//
// },
// "spacing":0,
// "tileheight":256,
// "tilewidth":256
// },
//
//
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using MiniJSON;

public class UnpackLevel : MonoBehaviour {

public TextAsset mLevelFile;
public int mNumberOfTileSets = 8;
public int mLevelScaling = 2;

private Dictionary<string, object> mJSONData;

//Stuff for loading
private long mLevelWidth;
private long mLevelHeight;
private List<string> mTileNames;

private List<int> mLevelLayerDataLocations = new List<int>();
private List<int> mStructuringDataLocations = new List<int>();
private List<int> mPlaceableDataLocations = new List<int>();
private List<int> mBackgroundDataLocations = new List<int>();
private List<int> mAvailiblePrefabs = new List<int>();
private List<int> mRobotDataLocations = new List<int>();
private List<int> mBudgetDataLocations = new List<int>();
private List<int> mInterractableDataLocations = new List<int>();

//Stuff for building
private List<GameObject> mLoadedResources = new List<GameObject>();
private List<GameObject> mSceneItems = new List<GameObject>();


void Start () {

mJSONData = MiniJSON.Json.Deserialize(mLevelFile.text) as Dictionary<string, object>;
UnpackTileSets();
UnpackLocations();
BuildLevelLayer();
/*BuildStructureLayer();
BuildPlaceableDataLocations();
BuildBackgroundDataLocations();
BuildInterractables();*/

}

void UnpackTileSets(){

mTileNames = new List<string>();

for(int i = 0; i < mNumberOfTileSets; i++){
mTileNames.Add(((mJSONData["tilesets"] as List<object>)[i] as Dictionary<string, object>)["name"].ToString());
}
}

// Each one of the for loops below are manually added for each new layer added to the editor
void UnpackLocations(){

mLevelWidth = (long)((mJSONData["layers"] as List<object>)[1] as Dictionary<string, object>)["width"];
mLevelHeight = (long)((mJSONData["layers"] as List<object>)[1] as Dictionary<string, object>)["height"];


for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[0] as Dictionary<string, object>)["data"] as List<object>)[i];
mLevelLayerDataLocations.Add((int)dataMiddleMan);
}

for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[1] as Dictionary<string, object>)["data"]as List<object>)[i];
mStructuringDataLocations.Add((int)dataMiddleMan);
}

for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[2] as Dictionary<string, object>)["data"]as List<object>)[i];
mPlaceableDataLocations.Add((int)dataMiddleMan);
}

for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[3] as Dictionary<string, object>)["data"]as List<object>)[i];
mBackgroundDataLocations.Add((int)dataMiddleMan);
}

for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[4] as Dictionary<string, object>)["data"]as List<object>)[i];
mAvailiblePrefabs.Add((int)dataMiddleMan);
}

for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[5] as Dictionary<string, object>)["data"]as List<object>)[i];
mRobotDataLocations.Add((int)dataMiddleMan);
}

for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[6] as Dictionary<string, object>)["data"]as List<object>)[i];
mBudgetDataLocations.Add((int)dataMiddleMan);
}

for(int j = 7; j < (mJSONData["layers"] as List<object>).Count;j++){
for( int i = 0; i < mLevelHeight*mLevelWidth; i++){
long dataMiddleMan = (long)(((mJSONData["layers"] as List<object>)[j] as Dictionary<string, object>)["data"]as List<object>)[i];
mInterractableDataLocations.Add((int)dataMiddleMan);
}
}
}

void BuildLevelLayer(){

IdentifyTiles();
InstantiateTheLevelObjects();
BuildSceneItems();


}

void IdentifyTiles(){

for(int i = 0; i < mNumberOfTileSets;i++){

if(Resources.Load ("Prefabs/" + mTileNames[i], typeof(GameObject)) != null){
mLoadedResources.Add (Resources.Load("Prefabs/" + mTileNames[i], typeof(GameObject)) as GameObject);
}
}
}

void InstantiateTheLevelObjects(){

for(int i = 0; i < mLevelLayerDataLocations.Count; i++){
GameObject tempObject;
int switchVariable = 0;

//This is a manual way for me to match each tile found to which type of item is being created for the Level layer Only
if(mLevelLayerDataLocations[i] == 0) switchVariable = 0;
if((mLevelLayerDataLocations[i]== 1) || (mLevelLayerDataLocations[i] == 2)) switchVariable = 1;//Conveyor
if((mLevelLayerDataLocations[i]== 3) || (mLevelLayerDataLocations[i] == 4)) switchVariable = 2; //Spring
if((mLevelLayerDataLocations[i] > 4) && (mLevelLayerDataLocations[i] < 9)) switchVariable = 3;//Fan
if(mLevelLayerDataLocations[i] == 9) switchVariable = 4;//Spawner
if(mLevelLayerDataLocations[i] == 10) switchVariable = 5;//Collector
if(mLevelLayerDataLocations[i] == 11) switchVariable = 6; //Robot

switch(switchVariable){

default:
break;

case 0:
break;

case 1: //Conveyor

tempObject = Instantiate(mLoadedResources[0],getInstantiatePosition(i), Quaternion.identity) as GameObject;
tempObject.name = mLoadedResources[0].name;

if(mLevelLayerDataLocations[i] == 1){
tempObject.transform.Rotate(0,180, 0);
}
break;

case 2: //Spring
tempObject = Instantiate(mLoadedResources[1],getInstantiatePosition(i), Quaternion.identity) as GameObject;
tempObject.name = mLoadedResources[1].name;

if(mLevelLayerDataLocations[i] == 3){
tempObject.transform.Rotate(0,180, 0);
}
break;

case 3: //Fan
tempObject = Instantiate(mLoadedResources[2],getInstantiatePosition(i), Quaternion.identity) as GameObject;
tempObject.name = mLoadedResources[2].name;

if(mLevelLayerDataLocations[i] == 5){
tempObject.transform.Rotate(0, 0, 90);
}

if(mLevelLayerDataLocations[i] == 7){
tempObject.transform.Rotate(0, 180, 0);
}

if(mLevelLayerDataLocations[i] == 8){
tempObject.transform.Rotate(0, 0, -90);
}
break;

case 4: //Spawner
tempObject = Instantiate(mLoadedResources[3],getInstantiatePosition(i), Quaternion.identity) as GameObject;
tempObject.name = mLoadedResources[3].name;

break;

case 5: //Collector
tempObject = Instantiate(mLoadedResources[4],getInstantiatePosition(i), Quaternion.identity) as GameObject;
tempObject.name = mLoadedResources[4].name;

break;

case 6: //Robot
tempObject = Instantiate(mLoadedResources[5],getInstantiatePosition(i), Quaternion.identity) as GameObject;
tempObject.name = mLoadedResources[5].name;

break;

}
}
}

Vector3 getInstantiatePosition(int incomingData){

Vector3 tempVector = new Vector3(0,0,0);

tempVector.x = (incomingData % mLevelWidth) * mLevelScaling;
tempVector.y = mLevelScaling * (mLevelHeight - (incomingData/mLevelWidth));

return tempVector;
}

void BuildSceneItems(){

GameObject mainCamera = Resources.Load ("Prefabs/Main Camera", typeof(GameObject)) as GameObject;
GameObject endGameCamera = Resources.Load("Prefabs/EndGameCamera", typeof(GameObject))as GameObject;
GameObject background = Resources.Load ("Prefabs/background", typeof(GameObject)) as GameObject;
GameObject light = Resources.Load ("Prefabs/Directional light", typeof(GameObject)) as GameObject;

//These are all of the vriables that need to be set on the Main Camera's scripts
GameObject tempObject = Instantiate(mainCamera, new Vector3(mLevelScaling*mLevelWidth/2,mLevelScaling*mLevelHeight,-15), Quaternion.identity) as GameObject;
tempObject.GetComponent<PlayerObjectButtons>().mEnableEachObject = EnablePlaceables();
tempObject.GetComponent<Budget>().mTotalBudget = SetBudget();
tempObject.GetComponent<MainCameraGUI>().mRobotsNeededToWin = SetRobotsNeededToWin();
tempObject.GetComponent<MainCameraGUI>().mMaxRobotsReleased = SetRobotsReleased();
tempObject.GetComponent<CameraMovement>().mLevelSize.x = mLevelWidth * 2;
tempObject.GetComponent<CameraMovement>().mLevelSize.y = mLevelHeight * 2;

Instantiate(endGameCamera, tempObject.transform.position, tempObject.transform.rotation);
Instantiate(light, light.transform.position, light.transform.rotation);
Instantiate(background, background.transform.position, Quaternion.identity);
}

bool[] EnablePlaceables(){

bool[] tempbool = {false, false, false};

for(int i = 0; i < mLevelHeight * mLevelWidth;i++){

if((mAvailiblePrefabs[i]== 1) || (mAvailiblePrefabs[i] == 2)) tempbool[2] = true;//Conveyor
if((mAvailiblePrefabs[i]== 3) || (mAvailiblePrefabs[i] == 4)) tempbool[0] = true; //Spring
if((mAvailiblePrefabs[i] > 4) && (mAvailiblePrefabs[i] < 9)) tempbool[1] = true;//Fan

}

return tempbool;
}

int SetBudget(){

int budget = 0;

for(int i = 0; i < mLevelHeight * mLevelWidth;i++){

if(mBudgetDataLocations[i] == 13) budget += 100;
if(mBudgetDataLocations[i] == 14) budget += 500;
if(mBudgetDataLocations[i] == 15) budget += 1000;
if(mBudgetDataLocations[i] == 16) budget += 10000;
}

return budget;
}

int SetRobotsNeededToWin(){

int robotsToWin = 0;

for(int i = 0; i < (mLevelHeight * mLevelWidth)/2;i++){

if(mRobotDataLocations[i] == 11) robotsToWin++;//11 is the value for the robot

}

return robotsToWin;

}

int SetRobotsReleased(){

int robotsReleased = 0;

for(int i = (int)(mLevelHeight*mLevelWidth)/2; i < (mLevelHeight * mLevelWidth);i++){

if(mRobotDataLocations[i] == 11) robotsReleased++;//11 is the value for the robot

}

return robotsReleased;
}
}

XML Conversation System

This conversation system will be  large portion of the game.  Because there will be a large number of conversations throughout the game, I did not want to have to program a new script for each one.  This meant I needed a format to store all of this information, and a system that could display and reveal the information correctly. I decided to use XML for this storage method.  After writing a simple sample XML, I discovered that Unity has its own built in XML parser.  Once I made a script in Unity's C# that could parse my sample XML, I set out to plan a complete conversation the player may have.  

Example of the conversation tree.png

First I would need a method to store and later recall what I parsed.  I decided on a Hub and response system.  In a normal hub system each hub points to a different hub and the player will progress through all answers just by saying everything at least once. My system has a middle section called a response that can point to multiple hubs based on a multitude of variables.  These variables can range from what item they used at that moment to how the AI feels about them.  These responses will also store the Items and information the player will receive when they say the correct things.

Before even creating the complete conversation XML, I needed to decide how i was going to store this data.  I created an object for each of the Hub and the response so I can continually build functions and customize what the player experiences throughout their conversation.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Hub {

public int hubID;
public List<string> displayOptions;
public List<int> displayOptionLinks;
public List<string> NPCOptions;

public Hub(int tempHubID){

this.hubID = tempHubID;
this.NPCOptions = new List<string>();
this.displayOptions = new List<string>();
this.displayOptionLinks = new List<int>();

}

//Display Options a link along with what the player will say
public void AddDisplayOption(string newDisplay, int newDisplayLink){
displayOptions.Add(newDisplay);
displayOptionLinks.Add(newDisplayLink);

}

public void AddNPCOptions(string newOption){
NPCOptions.Add (newOption);
}

}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Response {

public int responseID;
public string normalResponse;
public int link;

public Response(int tempResponseID, string tempNormalResponse, int tempLink){

this.responseID = tempResponseID;
this.normalResponse = tempNormalResponse;
this.link = tempLink;
}

}

Once I knew how I wanted to store the data, it was just a matter of figuring out how Unity parses XML.  Unfortunately for me, when I started this project, I did not know that Unity had its own parser, so this section took me a bit longer.  The final script used to parse my XML is shown below.

//  <hubID> 0 </hubID>
// <NPCOptions>
// <firstTime> This is what the NPC says the first time you talk to them in main hub</firstTime>
// </NPCOptions>
// <options>
// <option>
// <normalWords> This is option 1 in main hub</normalWords>
// <link> 1 </link>
// </option>
// <option>
// <normalWords> This is option 2 in main hub</normalWords>
// <link> 2 </link>
// </option>
// <option>
// <normalWords> This is option 3 in main hub</normalWords>
// <link> 3 </link>
// </option>
// </options>
// </hub>
//<response>
// <responseID> 1 </responseID>
// <normalResponse> This is response 1</normalResponse>
// <link> 1 </link>
//</response>
//</conversation>
using UnityEngine;
using System.Collections.Generic;
using System.Xml;

public class LoadXMLFile {

public static XmlDocument mXmlDocument = new XmlDocument();
public static List<Hub> mLoadedHubs = new List<Hub>();
public static List<Response> mLoadedResponses = new List<Response>();

public static Hub currentLoadingHub;
public static string currentNormalWords;
public static int currentOptionLink;
public static int currentHubLink;

public static Response currentLoadingResponse;
public static int currentResponseID;
public static string currentNormalResponse;
public static int currentResponseLink;

public static void AssignXMLDoc(string importedXmlTextDoc){

mXmlDocument.LoadXml(importedXmlTextDoc);
ReadHubs(mXmlDocument.SelectNodes("conversation/hub"));
ReadResponses(mXmlDocument.SelectNodes("conversation/response"));
}

static void ReadHubs(XmlNodeList hubNodes){

foreach(XmlNode node in hubNodes){
currentLoadingHub = new Hub(XmlConvert.ToInt16(node.SelectSingleNode("hubID").InnerText));
currentLoadingHub.displayOptions = new List<string>();
currentLoadingHub.displayOptionLinks = new List<int>();
currentLoadingHub.NPCOptions = new List<string>();

foreach(XmlNode option in node.SelectNodes("options/option")){
currentNormalWords = option.SelectSingleNode("normalWords").InnerText;
currentOptionLink = XmlConvert.ToInt16(option.SelectSingleNode("link").InnerText);
currentLoadingHub.AddDisplayOption(currentNormalWords, currentOptionLink);

}

foreach (XmlNode NPCOptions in node.SelectNodes("NPCOptions")){
currentLoadingHub.AddNPCOptions(NPCOptions.InnerText);
}
mLoadedHubs.Add(currentLoadingHub);
}
}

static void ReadResponses(XmlNodeList responseNodes){

foreach(XmlNode node in responseNodes){
currentResponseID = XmlConvert.ToInt16(node.SelectSingleNode("responseID").InnerText);
currentNormalResponse = node.SelectSingleNode("normalResponse").InnerText;
currentResponseLink = XmlConvert.ToInt16(node.SelectSingleNode("link").InnerText);
currentLoadingResponse = new Response(currentResponseID, currentNormalResponse, currentResponseLink);
mLoadedResponses.Add (currentLoadingResponse);
}
}

}

Once this system was setup, I needed a way for the system to know what to display.  Also, I needed to display this information on the NGUI buttons and labels that will be stored at the camera for each conversation.  I first attempted to use a state machine, but that proved unnecessary for what I initially wanted to do.  I suspect I will be going back to one as this increases in complexity.  I created a "Conversation" script that will take an xml file, load it and then will coordinate with a Button Manager script to make sure the correct information is displayed.  These two scripts are displayed below.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml;

public class Conversation : MonoBehaviour {

public TextAsset mXmlTextDocument;
public GameObject mConversationCamera;
public GameObject mGameManager;
public float mDisplayTime;
private float mDisplayTimeTracker;
private bool mIsHub;

private XmlDocument XmlDoc = new XmlDocument();
private List<Hub> mConversationHubs;
private List<Response> mConversationResponses;

//Prefabs for items recieved (need a more generic system later)
public GameObject mAlcoholPrefab;
public Inventory mInventory;

//How to track which Hub or Response to go to next
public int mCurrentLink = 0;


void Start() {

LoadXMLFile.AssignXMLDoc(mXmlTextDocument.text);
mConversationHubs = LoadXMLFile.mLoadedHubs;
mConversationResponses = LoadXMLFile.mLoadedResponses;

mConversationCamera.GetComponent<Conversation>().enabled = true;

mIsHub = true;
}

void Update() {

if(mIsHub){

this.GetComponent<ButtonManager>().UpdateHub(mConversationHubs[mCurrentLink]);

}
else{
if(!mIsHub){
DisplayResponse();
}
}
}

public void FindHubByID(int inputResponseLink){
for(int i = 0; mConversationHubs.Count > i; i++){
if(mConversationHubs[i].hubID == inputResponseLink){
mCurrentLink = i;
break;
}
}
mIsHub = true;
}

public void FindResponseByID(int inputHubLink){
for(int i = 0; mConversationResponses.Count > i; i++){
if(mConversationResponses[i].responseID == inputHubLink){
mCurrentLink = i;
break;
}
}

mIsHub = false;
}

void DisplayResponse(){

this.GetComponent<ButtonManager>().UpdateResponse(mConversationResponses[mCurrentLink]);
mDisplayTimeTracker += Time.deltaTime;

if(mDisplayTimeTracker >= mDisplayTime){

FindHubByID(mConversationResponses[mCurrentLink].link);
mDisplayTimeTracker = 0;
}
}
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ButtonManager:MonoBehaviour {


public GameObject mLabelLocation;
public GameObject[] mButtonLocations;

private Hub mCurrentHub;
private Response mCurrentResponse;
private bool mIfHub;

void Update(){

if(mIfHub){
CheckForButtonClicks();
}

}

public void UpdateHub(Hub importedHub){

mCurrentHub = importedHub;
mIfHub = true;
ChangePanelLabel();
ChangeButtonLabels(true);

}

public void UpdateResponse(Response importedResponse){

mCurrentResponse = importedResponse;
ChangeButtonLabels(false);
mIfHub = false;
ChangePanelLabel();

}

void ChangePanelLabel(){

if(mIfHub){
mLabelLocation.GetComponent<UILabel>().text = mCurrentHub.NPCOptions[0];
}
else{
mLabelLocation.GetComponent<UILabel>().text = mCurrentResponse.normalResponse;
}

}

void ChangeButtonLabels(bool buttonsOn){

if(buttonsOn){

int numberOfButtons = mCurrentHub.displayOptions.Count;

for(int i = 0; i<mButtonLocations.Length;i++){
mButtonLocations[i].SetActive(true);
}

for(int i = 0; i<numberOfButtons; i++){
mButtonLocations[i].GetComponentInChildren<UILabel>().text = mCurrentHub.displayOptions[i];
}
if(numberOfButtons < mButtonLocations.Length){
for(int i = numberOfButtons; i<mButtonLocations.Length;i++){
mButtonLocations[i].SetActive(false);
}
}
}
else{
for(int i = 0; i<mButtonLocations.Length;i++){
mButtonLocations[i].SetActive(false);
}
}
}

void CheckForButtonClicks(){

for(int i = 0; i< mButtonLocations.Length; i++){
UIEventListener.Get(mButtonLocations[i]).onClick += ButtonClicked;
}
}

void ButtonClicked(GameObject button){

for(int i = 0; i < mCurrentHub.displayOptions.Count; i++){
if(mCurrentHub.displayOptions[i] == button.GetComponentInChildren<UILabel>().text){
gameObject.GetComponent<Conversation>().FindResponseByID(mCurrentHub.displayOptionLinks[i]);

break;
}
}
}
}

This system was probably the most time consuming and challenging task I have completed thus far for my Investigative Adventure.  It will be revisited as I implement more of the features that I believe will make for an engaging system.  Feel free to contact me about this project with any questions.