Sunday, September 22, 2013

Unit Selection Prefabs

10 Unit animated selection prefabs:

  • 1 Projector-based
  • 3 Quad-based
  • 6 Particle-based
23 textures

Fully colorizable

Click here for the Web Demo.


Monday, September 16, 2013

Tiles of War - Web Demo

Getting ready to submit another asset to the Unity Asset Store.

It's a pretty complete tile-based Fog of War system that automatically handles terrain and unit visibility and "ghosting".   It supports rendering fog via either Image Effect (Unity Pro only), Projector (Unity Standard or Pro), or Plane Overlay (Unity Standard or Pro).

I'll talk more about all of that when I get a chance.

Click for Web Demo

Monday, September 2, 2013

Still Waiting...

Still waiting for the Procedural Geometry Dungeon Generator to be approved for the Unity Asset Store.

I'm not clear on how long this should be taking... so I'm going to assume a week is normal.

If it hasn't been approved (or rejected) by Tuesday I guess we should send them an email.

Saturday, August 24, 2013

Dungeon Generator

Let's talk a little about the algorithm we use to generate the dungeon itself.  

I'm talking about the dungeon not the meshes, lighting, etc.

Early on we decided that our little dungeons would have floors, walls, pits, and bridges as the main building blocks.  So whatever algorithm we use needs to generate those blocks.

Before starting this project, I Googled "dungeon generator".   Wow.  Lots of results and lots of interesting reading.  Try it yourself.

But in the end, I decided to punt and come up with what seemed like it would generate an interesting dungeon without being too terribly hard to code.   I'm certain it isn't unique or original, by any means, but I didn't use anyone else's source for it.   I'm not going to go into the specifics of the code right now, just the general algorithm.

So, how do we get started?

First, we need some corridors for our player to run around in, so let's generate a maze...


A plain maze would make running around in the dungeon a bit annoying, so we'll add some "extra" corridors.


Now that are base paths are generated, we can add big gaping chasms to it, just for variety...


... and rooms ...


Hmmm... maybe add a bit more "pit".


And finally, doors and columns.


And there you have it!

And I'll leave you with one more screenshot of a 1024x1024 dungeon (which runs just fine in Unity, by the way, but it takes several seconds to build all the meshes):


Wednesday, August 21, 2013

Procedural Geometry Dungeon Generator

Over the last week we've been working on a procedural dungeon generator.  Not really for any particular game, just as a "hey, this is kinda neat" project.

I wish I'd been posting about it as it's progressed, but then I'd have less time for coding :)

This is just a quick catch-up post about the progress so far.

. . .

But first, what exactly is "Procedural Geometry"?

Well, instead of using models or prefabs in Unity we build the meshes ourselves, specifying the vertices, indices, texture coordinates, etc.

Why would we do it that way?

Well, for one thing it's blazingly fast.  We can generate a 256x256 "tile" dungeon at run-time, geometry and all, in well under a second.   And believe me, that's a big dungeon when you start moving around in it.

Doing a dungeon that large with prefabs, at run-time is problematic for a number of reasons if you don't approach it in the right way (but it can definitely be done).   The first approach most people think of is to just use GameObject.Instantiate to add prefabs.   But a 256x256 dungeon ends up adding over 65,000 objects to the scene.  It works, and Unity can handle it, it's just slooow.   (Note: instantiation isn't really what slows it down, it's the voodoo that Unity does between the Start and first Update, at least according to the timings we've done while playing with the process.)

So let's do this procedurally for now... and see what happens.

We want to support dungeons up to 1024x1024 (or bigger?!) so we did end up "chunking" the dungeon by breaking it up into multiple meshes.   We had to do that due to Unity's limit on vertices in a single mesh.

But the chunking also helps out with performance by letting Unity frustum cull and only render the chunks that are visible.

But none of that is the fun part... let's talk about how we've progressed the generator.

. . .

Click the images below for bigger views.

First steps:  build the basic geometry of the dungeon...



Not exactly exciting, so let's spice it up a bit by adding pits and bridges.



Better... but still nothing to write home about.  It's a little flat looking.  Obviously textures would help, but we'll put that off for now.

Speaking of textures, you may notice the columns for the bridges are just hanging there.   We'll take care of that later with texturing (or possibly a custom shader).

So what can we do to make the geometry more interesting?   Hmmm.. how about borders around all the edges.



It's getting there, but even the borders are boring... so we'll spruce them up.



There.  The basic geometry is okay (at least for what we're trying to do).

So what now?

Let's texture it!  I think we're going for a "retro pixel" look, but still keep it "kinda-sorta realistic" (meaning no 8-bit bright colors).

And let's be honest here, "retro pixel" is attractive to me because I can't make decent high-res textures to save my life!



The single texture used above is 128x128.  Nice and tiny.  We could scale that up to add more details, but that works well for the "retro pixel" look we're shooting for.



Notice the black fade... that's saves our "dangling bridges and pits" from looking weird.   The texture is a little wasteful, so we may come back to that later.   We could add semi-transparent water or lava or green slime as well and it would still look nice.

But let's get back to the dungeon...  It's still not looking quite the way we want it.

How about some lighting?   We're using a projector (Unity built-in) to color and light the dungeon.   Why?  Well, it'll let us provide "Fog of War" type lighting easily if we want to later in the project.



But wait... what dungeon is complete without columns...



... and doors.


Yeah, it makes absolutely no sense to put the doors in as static geometry.  They should be placeable prefabs.  *shrug*

And let's test by generating a 256 x 256 dungeon:


. . .

And that's it for now.

Next time, I'll discuss adding colliders and walk-meshes and how we actually generate the dungeon data.

The current version works both generating the dungeon inside the editor (useful for adding walk-meshes) or at run-time (useful for random "rogue-like" games).

We'll also support saving to and loading from files.

Customizing RTS Camera Behavior

So a few people have purchased RTS Camera Pro, which is awesome.  I'd love some feedback if anyone is using it (especially if it actually makes it into a production game).

And speaking of feedback...

One of the purchasers asked how he'd go about zooming and moving the camera using on-screen controls and zooming in via double-clicks (paraphrased, obviously).

The script below does this.   It's obviously raw demo-esque code (it took all of ten minutes to write and I didn't bother really commenting it very well).   The GUI is embedded in the script and would probably be broken out if this was for a "for realz" application, but this is fine for testing.

It allows:

  • Pan via left-mouse button down (click and drag)
  • Zoom on double-click
  • Movement and zooming via on-screen GUI

The only "tricky" part (and it wasn't really much of a trick) was detecting double-clicking.   This way works, but it can certainly be improved upon.

I also added checking to see if the user is inside the GUI area so that clicking in the GUI doesn't pan.   If you know a better way to do this, please let me know.

Just attach the script (below) to the Main Camera (or any other scene object, really) and it'll do the trick.

Here's the script:

 using UnityEngine;  
   
 public class CustomCamera : MonoBehaviour  
 {  
   private RtsCamera _rtsCamera;  
   private RtsCameraMouse _rtsCameraMouse;  
   private RtsCameraKeys _rtsCameraKeys;  
   private Rect _guiRect;  
   
   float _doubleClickStart = 0;  
   
   private bool _prevMouseDown = false;  
   
   void Start()  
   {  
     _rtsCamera = Camera.main.GetComponent<RtsCamera>();  
     _rtsCameraMouse = Camera.main.GetComponent<RtsCameraMouse>();  
     _rtsCameraKeys = Camera.main.GetComponent<RtsCameraKeys>();  
   
     // we're going to change our zoom speed programmatically, just for testing... should really do this in the Unity editor  
     _rtsCameraMouse.ZoomSpeed = 5000;  
     _rtsCameraKeys.ZoomSpeed = 200;  
   
     // start out zoomed ALL the way out... mainly because we're testing zooming in with double click  
     _rtsCamera.Distance = _rtsCamera.MaxDistance;  
   
     const int areaWidth = 350;  
     const int areaHeight = 200;  
     _guiRect = new Rect((Screen.width - areaWidth) / 2f, Screen.height - areaHeight - 64, areaWidth, areaHeight);  
   }  
   
   void Update()  
   {  
     // check to see if the user is pressing (holding) mouse button 0 (left mouse)  
     if (Input.GetKey(KeyCode.Mouse0))  
     {  
       if (!IsInsideGui()) // we ONLY pan when the user is NOT inside the gui area(s)  
       {  
         _prevMouseDown = true;  
   
         var panX = -1 * Input.GetAxisRaw("Mouse X") * _rtsCameraMouse.PanSpeed * Time.deltaTime;  
         var panZ = -1 * Input.GetAxisRaw("Mouse Y") * _rtsCameraMouse.PanSpeed * Time.deltaTime;  
   
         _rtsCamera.AddToPosition(panX, 0, panZ);  
       }  
     }  
     else  
     {  
       if (_prevMouseDown)  
       {  
         OnMouseUp();  
         _prevMouseDown = false;  
       }  
     }  
   }  
   
   /// <summary>  
   /// Checks to see if the mouse is within the gui area. We don't want to pass the left clicks through if it is.  
   /// </summary>  
   /// <returns>True if the mouse is currently inside the gui area, False otherwise.</returns>  
   private bool IsInsideGui()  
   {  
     var ix = Input.mousePosition.x;  
     var iy = Input.mousePosition.y;  
     var transMouse = GUI.matrix.inverse.MultiplyPoint3x4(new Vector3(ix, Screen.height - iy, 1));  
   
     return _guiRect.Contains(new Vector2(transMouse.x, transMouse.y));  
   }  
   
   /// <summary>  
   /// Render the simple test UI  
   /// </summary>  
   void OnGUI()  
   {  
     var t = Time.deltaTime;  
     var speed = _rtsCameraKeys.MoveSpeed;  
     var rotSpeed = _rtsCameraKeys.RotateSpeed;  
     var tiltSpeed = _rtsCameraKeys.TiltSpeed;  
   
     GUILayout.BeginArea(_guiRect);  
   
     GUILayout.BeginHorizontal("box");  
     GUILayout.Space(16);  
   
     GUIControls(speed, t, rotSpeed, tiltSpeed);  
   
     GUILayout.Space(16);  
     GUILayout.EndHorizontal();  
     GUILayout.EndArea();  
   }  
   
   private void GUIControls(float speed, float t, float rotSpeed, float tiltSpeed)  
   {  
     GUILayout.BeginVertical();  
   
     GUILayout.Space(16);  
   
     GUIMovementButtons(speed, t);  
   
     GUILayout.BeginHorizontal();  
     GUIRotateButtons(t, rotSpeed);  
     GUILayout.Space(16);  
     GUITiltButtons(t, tiltSpeed);  
     GUILayout.EndHorizontal();  
   
     GUILayout.Space(16);  
   
     GUIZoomSlider();  
   
     GUILayout.Space(16);  
   
     GUILayout.EndVertical();  
   }  
   
   private void GUIZoomSlider()  
   {  
     GUILayout.Label("Zoom");  
     _rtsCamera.Distance = GUILayout.HorizontalSlider(_rtsCamera.Distance, _rtsCamera.MinDistance, _rtsCamera.MaxDistance);  
   }  
   
   private void GUITiltButtons(float t, float tiltSpeed)  
   {  
     if (GUILayout.RepeatButton("Tilt Up"))  
     {  
       _rtsCamera.Tilt += t*tiltSpeed;  
     }  
     if (GUILayout.RepeatButton("Tilt Down"))  
     {  
       _rtsCamera.Tilt -= t*tiltSpeed;  
     }  
   }  
   
   private void GUIRotateButtons(float t, float rotSpeed)  
   {  
     if (GUILayout.RepeatButton("Rotate Left"))  
     {  
       _rtsCamera.Rotation += t*rotSpeed;  
     }  
     if (GUILayout.RepeatButton("Rotate Right"))  
     {  
       _rtsCamera.Rotation -= t*rotSpeed;  
     }  
   }  
   
   private void GUIMovementButtons(float speed, float t)  
   {  
     GUILayout.BeginHorizontal();  
   
     if (GUILayout.RepeatButton("Left"))  
     {  
       _rtsCamera.AddToPosition(-speed*t, 0, 0);  
     }  
     if (GUILayout.RepeatButton("Forward"))  
     {  
       _rtsCamera.AddToPosition(0, 0, speed*t);  
     }  
     if (GUILayout.RepeatButton("Back"))  
     {  
       _rtsCamera.AddToPosition(0, 0, -speed*t);  
     }  
     if (GUILayout.RepeatButton("Right"))  
     {  
       _rtsCamera.AddToPosition(speed*t, 0, 0);  
     }  
   
     GUILayout.EndHorizontal();  
   }  
   
   void OnMouseUp()  
   {  
     if ((Time.time - _doubleClickStart) < 0.3f)  
     {  
       this.OnDoubleClick();  
       _doubleClickStart = -1;  
     }  
     else  
     {  
       _doubleClickStart = Time.time;  
     }  
   }  
   
   void OnDoubleClick()  
   {  
     // get position the user double clicked (on plane)  
     var ray = Camera.mainCamera.ScreenPointToRay(Input.mousePosition);  
     var fDist = 0.0f;  
     RaycastHit hitInfo;  
     if (Physics.Raycast(ray, out hitInfo))  
     {  
       // we hit something!  
   
       // move the user to the double-clicked position  
       _rtsCamera.JumpTo(hitInfo.point);  
   
       // zoom in "about 25%"  
       var dist25Percent = (_rtsCamera.MaxDistance - _rtsCamera.MinDistance)*0.25f;  
       _rtsCamera.Distance -= dist25Percent;  
     }  
   }  
 }