Thursday, September 02, 2010

Knight's Quest, the story so far...

Sorry we've been a bit quiet here recently, both Olli and I have been beavering away like a couple of mentals on projects which hasn't given us any free time to devote to the blog ( Also we didn't have that much of interest to say ).

Knight's Quest, our first Facebook and our first RPG, has been my main focus for weeks. It's finally starting to come together after weeks of just working on the techy side, so I thought it may be of some interest to go over some bits and bobs from it.

The game is iso, which is a pain at the best of times. Z-sorting is murder. How I've gone about it is to split the map into sectors. Each sector is 16x16 tiles, and we only z-sort on those visible areas ( Also we turn off any plotting in sectors which aren't visible ).
This really reduces the sorting to just what is needed, also the floor tiles are burned into their own bitmap ( See this old post ) so we're only having to sort tiles and sprites, which helps.
Also as we're using the blitter we don't have to worry about sequential indexes, it's like as2 all over again, where you could give a sprite any old depth and not care about it.

Next up, the dungeons. I've used Olli's generator ( He's written tons about it previously ) and it works a treat. Basically it creates "cells" which are in this case 4x4 tiles. The cells tell us which walls or doors are present. Rather than just randomly plot tiles I've set up a lot of mc blocks, eg.

KQ_tiles.png
This may be tricky to explain well so stick with me. As we read each cell we know what's there, let's say for example a north and east wall. From there we look up the array which contains all the blocks with a north and east wall and pick one at random.
The floor is burnt into the background bitmap and the wall tiles ( Or cauldron placeholders ) are converted into blitter sprites ( Bobs ) which can then be plotted / sorted etc.
The reason for this is that we can then hand design a lot of different wall blocks and use them randomly ( Rather than just picking one random tile at a time ). It should make the dungeons look more hand designed, which is what we want.
It's important that we create the impression that someone has slaved over each dungeon to make it look as non-random as possible.

I think that's about it for now, there will be more to share real soon.

Squize.

#    Comments [3] |
 
Friday, June 25, 2010

first post in ages - and a jolly short one too.

I guess you won't remember me (nGFX) as for ages only Squize has done all the posting. Not that I have been lazy, but there hasn't been a single line of interesting code on my side, nor something mildly game related.

I've been coding some large scale foto archive software. To sell our really vast collection of black/white press fotos (180 000 of the 1.5 million negatives should be available in the end) and coding the backend and the frontend (with lots of ajax). Right now last bits and bobs of the user handling needs to be done and there are still a good number of negatives to be scanned (alas, thank fuck I'm not in charge there).

fotoarchiv_00.jpg

Ok, back to something game related at last ...

While Squize is churning out flash games like mad, I've jumped on the Unity train (of course) and been tinkering with it for a while now. I've started with porting the dungeon creation code over to C# and played with dynamic maps, which proved to be working just nicely. In order to get more out of this project I thought it might be helpfull to get deeper into Unity with smaller game.

A remake of one of my games seemed a good idea, I've got the AS2 code to look at and knew how the game should work (and added some minor additions). So here's something I've learned besides the usual "oh damnit" ...

Unity's animation editor is good enough to make things easier, but don't trust it on time critical problems.

The game involves things moving along tracks - so my first idea was to save some code and use animations for that. I wanted to use a container that I can just move from tile to tile and let the "car" move inside it from the start of the tile to the end of it, then an animation event should be fired, telling the engin to move the "car" to the next tile and start the correct animation.

I did a few tests (of course) and it seemed to work, so I did all the animations (straight tracks, junctions, ramps and so on) which roughly saved some 500 lines of code (compared to the flash version) - oh lovely - same game, less code, I was on fire.

Of course it didn't work.

I tested it locally, online in a browser and all went well until I noticed that the timing can get off the track and cause visual glitches. These showed themself as "jumping" car, where the car jumped ahead one tile for a single frame and then continued as intended. This happened every time I started a level - but not always at the same time (hence I didn't see it in my tests).
After a good night of debugging, tracing (or Debug.Log()) I found out what happend.

Unlike flash the "timeline" in Unity doesn't sync visuals and code - and in fact Unity has no "timeline" - point taken, lesson learned :| .
So this happened (and caused the glitch)
1. [engine] sendMessage -> [car] "goto next tile"
2. [car] move to next tile, rewind animation, play it
3. (glitch might occur)
4. [car] sendMessage -> [engine] "done, give me next tile"
...

Because the code isn't tied to the visuals, the code in 2 can be executed, but the visuals from the animation might start on the next frame, hence the container is moved to the next tile, but the animation is still on the last frame (at the and of the track) ...

... I ended up coding the movement in the end ...

Editor scripts can do a lot of damage (or just be utterly helpfull)

In order to get the levels into the game I needed an level editor, but was too lazy to write one - so I decided to dig into editor scripts in Unity, which allow you to do all kind of dangerous things.
I wanted to be able to drag my level into the Unity editor window, grab it and save it to a file (which works just wonderfull). the first big "shit" came when I added a function to clear the level from screen (so I could do a new level) and carelessly allowed "DestroyImmediate" to delete from the asset window (and not checking if the GameObject I want to destroy is a child of my Playground) - oh well.

Anyway, you can easily add you own menu entries, access files or manipulate your current scenes with editor scripts.

And now some screenies ...

Project Hellstrom

hellstrom_00.jpg
The ponytailed lady is just my scaler model, in the end I guess it'll be 1st person.
Right now you can walk around a dynamic generated map (with temp mapping) - A LOT of work left to do.


ToyGame - the game without a name yet

toygame_00.jpg
Inside Unity's editor, the first level in progress ...

toygame_01.jpg
Playing level 1, just crashed 2 toys ...

And with this I descent back into the hell that is js/css and html ...

nGFX





#    Comments [0] |
 
Tuesday, March 16, 2010

The good things in life aren't for free ...

Let's start with a little rant.

So there are all these shiny things you can do with css, alas they might not work in all the browsers, knowing what works (or to know how to fake/correct it) is for me some sort of art form. I'm not even talking about the possibilites of JS and DOM based development.
But I do believe that there is something utterly wrong with css in parts if there are people out there (thanks guys) who devote a damn good deal of time to write bloody FRAMEWORKS so that you can do something as trivial as a 3 colum layout. It gets worse if you want a header and a footer - alas not a footer that is always BELOW each of the 3 columns and not just the main column).

I needed to get that out.

What else? Ah the redesign - yet again - what first seemed a good idea, wasn't that good when done. Back to start then. (Meanwhile we just continue here.)

Speaking of things that seemed to be a good idea ...

Last time I wrote about the map format used for the current game (or that I'm thinking about it) the last one I had seemed to be a good idea too. I guess I can say that I was wrong :) .

My first idea was to a tilebased map and to create it from the dungeon data I've got out of the dungeon creation code. Basically I wanted to convert each cell into an 6x6 piece of tiles, so there could have been a wall 1 tile wide and so on.
This worked quite well until I had the idea that I don't need a tile based map at all and just could plot the floor of a cell in one go (as for the new idea I don't needed walls in the tiles) - this worked also quite well.

I could reduce the code needed to convert the dungeon by some 75% - and was quite please with the outcome as it was also quite fast, the scroller needed to plot less tiles and everybody was happy, I used simple vector math for wall/door/trigger collision tests. Untill ...

While recoding the converter there was a nagging feeling that I forgot something, that I *needed* the tile information for something and right just as I finished the converter I dwaned on my why I wanted to use them in the first place - pathfinding.

Darn.

I know I could use nodebased pathfinding or some other utterly clever method (I've got heaps of books about that) but honestly there is nothing as simple as a tilebased pathfinding ... so I'm adding a simple tile map info back in (using a bool map). Joy.

Well, good thzat I haven't just deleted the old code ...

nGFX

#    Comments [0] |
 
Thursday, August 06, 2009

Dial Z for Zombie - dev diary

So after the set of articles about random level generation I wanted to put that into use (alas not in the scale I planed it out - mind you) - so I came up with a nice little 3 week project - that was 5 weeks ago.

The basic idea is a simple top/down shooter blended with a good portion of Gauntlet added (hordes of enemies, maybe). And while it's fun to watch levels being created by code it involves a good deal of additional work, like changing the tilesets, adding dirt and stains.

The X entries seemed to be a good idea, so I'm going to publish a few wip builds soon, too (alas not as many as Squize).

In order to see something at all (except the test visuals from the article tests) I needed to convert the cell based dungeons into someting tile based, and write a simple scroller to display the created maps. I decided that each cell should consist of 6 by 6 tiles, so I could use a one tile wide border for the walls (if any) and still have 4 tiles walking space. Next point on the list was the question about how to store the data generated - as I didn't wanted to convert cells/tiles at runtime before they're being displayed. Storing them in an array seemed a good idea, but I wanted to try something new...

Not to mention that converting 50x50 cell dungeon would result in an 300x300 array.

After a few minutes I thought that using BitmapData would be a nice try to store the map, as it'll give me a quite quick direct access 3 dim array (x,y, R,G,B,A) which I could use multiple times. so I used the alpha chanel for the tileset, the red one for the actual tile, blue for pathfinding and green for effects/2nd layer.

Right, why storing a tileset?

In my idea each room has basically the same tiles (ie. walls, doors, corners and floor) and the conversion would be much easier if I would use the same tiles over and over. So I came up with storing tiles in tilesets, which offsets the tiles on the tilesheet.
The creation process is quite easy this way:

First I have to create a tileset and give it a name, say "Corridor".

myTileset = new Tileset("Corridor");

Then add tiles to that tileset, giving it a name and and x/y offset in the tilesheet (I wrote data class for that):

myTileset.addTile(new Tile("empty", 0, 0));
myTileset.addTile(new Tile("Floor00", 1, 0));
myTileset.addTile(new Tile("WallN", 0, 1));
myTileset.addTile(new Tile("CornerInN", 1, 1));


Internally I use the tile's index, but the cell/tile converter just uses the name:

if (!myCell.isUnused) {
                        
    iTile = myTileset.index(strFloor);
    iPath = 0
    iSpecial = 0;
    bmpdMap.fillRect(new Rectangle(xx, yy, iTilesPerCell, iTilesPerCell), this.rgba(iTile, iSpecial, iPath, iTileset));
    
    // walls
    for (i = 0; i < Dir.NUM_BASEDIR; i++) {
        
        if (!myCell.isOpen(i)) {
            cx = xx + aWall[i].x;
            cy = yy + aWall[i].y;
            
            iTile = myTileset.index("Wall" + Dir.shortName(i));
            iPath = 255;    // so you can't walk there (for the bool map pathfinding)
            iSpecial = 0;
            bmpdMap.fillRect(new Rectangle(cx, cy, aWall[i].width, aWall[i].height), this.rgba(iTile, iSpecial, iPath, iTileset));
            
            // door
            if (myCell.hasDoor(i)) {
                
                // door frames are painted "above" the floor, so I use the special layer for that
                iTile = myTileset.index(strFloor);
                iPath = 127;
                iSpecial = myTileset.index("Door" + Dir.shortName(i) + "0");
                bmpdMap.setPixel(cx + aDoor[i][0].x, cy + aDoor[i][0].y, this.rgba(iTile, iSpecial, iPath, iTileset));
                
                iSpecial = myTileset.index("Door" + Dir.shortName(i) + "1");
                bmpdMap.setPixel(cx + aDoor[i][1].x, cy + aDoor[i][1].y, this.rgba(iTile, iSpecial, iPath, iTileset));
                
            } // ToDo: add windows if possible ...
            
        }
        
    }
    
    // ... draw corner and outer coners [skipped]
}

Ah. Nice and easy :)

So by using a different tileset you can easily control the visuals of a room.

Next thing on the list: the scroller - more thinking here ...

I wrote the Scroller class as extension to Sprite so I could add my own sprites and use the Sprites scrollrect for clipping. Adding a Scoller to the stage became easy as 123:

this._Scroller = new Scroller(620, 460, 20, 20); // width, height, tilewidth and tileheight
this._Scroller.name = "scroller";
this._Scroller.x = 10;
this._Scroller.y = 10;

this.addChild(this._Scroller);

this._Scroller.setTileset(this._bmpdTileset, this._TilesetCollection);
this._Scroller.setMap(this._bmpdMap); // the freshly generated map
this._Scroller.setCenter(310, 230);   // alows offsettin the origin, so 0,0 can be at the center of the scroller

this._Scroller.xPos = 0;
this._Scroller.yPos = 0;
this._Scroller.draw();
// updates the visuals of the scroller

the way the scroller is set up (I still need to optimize redrawing, though) makes it possible to use it with tween utils like TweenLite:
TweenLite.to(this._Scroller, 1, [xPos: 100, yPos: 100, onUpdate: this._Scroller.draw});

So after a few days of coding it looks like this:
DialZ_pre_00_small.jpg
(scaled version)

The visibilty test is in place (you can only see the room you're currently in and vector boundaries are created (the green rect in the room, door triggers (blue rect)). The map in the left corner is shwoing the pathfinding bounds (helpfull for testing, too) and will be replaced by a minimap later (circular, hopefully).

Right now I'm writing the vector intersection methods (I'm using math instead of the tiledate for collisions) - so I thing the next entry will feature that.

nGFX

#    Comments [1] |
 
Tuesday, July 14, 2009

Random Dynamic Levels - Part 3

In the last two articles  we made a maze and then destroyed it by adding a lot of free space. In part 3 of this articles I'm going to add some rooms to the empty space and add some doors ...

Part 3 - part 1 - why seperate things and make rooms?

At a first glance it might not be necessary to seperate data into dungeon, room and cell, but thinking ahead a bit it should make sense ...
My idea is that you can have the dungeon which holds the complete map (with the basic room data rendered into it) and the rooms so you access them easier and most important do some magic with them later. One of the neat things is that you could use a different tiling for rooms making them more detailed or use the additional map data for skinning (when rendering the dungeon into a tile based map).

The main difference between a room and a dungeon is that the room consists of empty cells and has walls along it's boundaries. So the first additional method we'd add to the Room class will be the init method, which simply sets all cells so they form a rectangular room.

public function initCells ():void {
            
    var x:uint;
    var y:uint;
    var cellTmp:Cell;
            
    for (x = 0; x < this.iWidth; x++) {
        for (y = 0; y < this.iHeight; y++) {
                    
            cellTmp = this.cell2(x, y);
            cellTmp.setWalls(WallType.OPEN);
                  
            if (x == 0) cellTmp.setWall(Dir.WEST, WallType.WALL);
            if (x == (this.iWidth - 1)) cellTmp.setWall(Dir.EAST, WallType.WALL);
            if (y == 0) cellTmp.setWall(Dir.NORTH, WallType.WALL);
            if (y == (this.iHeight - 1)) cellTmp.setWall(Dir.SOUTH, WallType.WALL);
        }
    }
}

We could later add methods to create different rooms (ie. two overlapping rectangles, circular rooms), but for now that will do ...

I also added a getter setter for Offset to the room, so we can modify the x and y pos of the bounding rect that we stored in the map class (you'll see later what's that for).

So we now have a room, but how do we get that sucker into the map, or (as just putting it into the map isn't really an issue) where can we place it *best*.

There are a few things that I want to watch when placing the rooms ...
- a room should not overlap any existing room, we rather don't at it
- placing a room at a place where it doesn't touch anything, is something we don't want, too
- rooms overlapping corridors should be avoided
- rooms touching dead ends is something we want (what's nice than finding a room after a long winded corridor?)
- rooms touching any sort of wall is OK, too

Yet again we (as humans) could just look at the map and say "here, there and there" and done, but that stupic piece of plastic cannot... we need to apply some sort of scoring to the whole placement mess.

Here's a bit of pseudo code ...
  • start with a VERY high best score, lets say 999999999999 and set current score to 0 ...
  • Loop over every cell in the dungeon
    • at any given position check if the new room overlaps any rooms already in there
      if so, we add 5000 to our current score, otherwise we add nothing
    • now loop over every cell in the room and compare it with the current dungeon cell (offsetting the room to the current position)
      • if the current room cell touches an empty cell (in the dungeon), add 10
      • if we touch a wall, add 3
      • if we touch a dead end, add 1
    • if the final current score is lower than the best score, sreplace the best score and store the current position as the best possible location
  • if the score is higher than the "room overlaps room" score, assume that it only can be placed overlapping a room and drop it, otherwise add it to the dungeon
Here is the scoring code:

private const TOUCH_DEADEND:int = 1;
private const TOUCH_WALL:int = 3;
private const TOUCH_EMPTY:int = 10;
private const OVERLAP_ROOM:int = 5000;
private const OVERLAP_CORRIDOR:int = 100;
        
public function fitMazeRoom (myRoom:Room):Boolean {
    
    var iBestScore:int = 999999999;
    var iScore:int = 0;
    var pBestPos:Point = new Point(0, 0);
    var pOffset:Point = new Point(0, 0);
    
    var cellDungeon:Cell;
    var cellNext:Cell;
    
    var rectTmp:Rectangle = myRoom.rectBound.clone();
    
    var i:uint;
    
    var x:int;
    var y:int;
    
    var xx:int;
    var yy:int;
    
    var iRoomID:uint;
    
    var bAddRoom:Boolean = false;
    
    // loop over map (- roomsize)
    for (y = 0; y < (this.iHeight - myRoom.iHeight + 1); y++) {
        for (x = 0; x < (this.iWidth - myRoom.iWidth + 1); x++) {
            
            // do the scoring ...
            iScore = 0;
            
            // check room/room overlapping
            rectTmp.x = x;
            rectTmp.y = y;
            for (i = 0; i < this._aRoom.length; i++) {
                if ((this._aRoom[i] as Room).rectBound.intersects(rectTmp)) {
                    iScore += OVERLAP_ROOM;
                }
            }
            
            // check room/dungeon overlapping
            for (yy = 0; yy < myRoom.iHeight; yy++) {
                for (xx = 0; xx < myRoom.iWidth; xx++) {
                    pOffset.x = (x + xx);
                    pOffset.y = (y + yy);
                    
                    cellDungeon = this.cell(pOffset);
                    if (cellDungeon.iType == RoomType.CORRIDOR) iScore += OVERLAP_CORRIDOR;
                    
                    if (yy == 0) {
                        iScore += this.getCellScore(this.cell(this.getNextPos(pOffset, Dir.NORTH)), Dir.NORTH);
                    }
                    if (xx == (myRoom.iWidth - 1)) {
                        iScore += this.getCellScore(this.cell(this.getNextPos(pOffset, Dir.EAST)), Dir.EAST);
                    }
                    if (yy == (myRoom.iHeight - 1)) {
                        iScore += this.getCellScore(this.cell(this.getNextPos(pOffset, Dir.SOUTH)), Dir.SOUTH);
                    }
                    if (xx == 0) {
                        iScore += this.getCellScore(this.cell(this.getNextPos(pOffset, Dir.WEST)), Dir.WEST);
                    }
                }
            }
            
            if (iScore < iBestScore) {
                iBestScore = iScore;
                pBestPos = new Point(x, y);
            }
            
        }
    }
    
    // add to dungeon if it doesn't overlap any other rooms
    if (iBestScore < OVERLAP_ROOM) {
        myRoom.pOffset = new Point(pBestPos.x, pBestPos.y);            
        bAddRoom = true;
    }
    
    return bAddRoom;
    
}

private function getCellScore (cellNext:Cell, iDir:int):int {
    
    var iScore:int = 0;
    
    if (cellNext.iType == RoomType.CORRIDOR) {
        if (cellNext.isDeadEnd) {
            iScore += TOUCH_DEADEND;
        } else if (cellNext.hasWall(Dir.getOppositeDir(iDir))) {
            iScore += TOUCH_WALL;
        } else {
            iScore += TOUCH_EMPTY;
        }
    } else {
        if (cellNext.iType == RoomType.ROOM) {
            if (cellNext.hasWall(Dir.getOppositeDir(iDir))) {
                iScore += TOUCH_WALL;
            } else {
                iScore += TOUCH_EMPTY;
            }
        } else {
            iScore += TOUCH_EMPTY;    
        }
    }
    
    return iScore;
    
}

That's ugly and not very fast, but it works.

Some additional info about adding the room to the dungeon: whenever we place a room cell in the dungeon map it's a good idea to check if it overwrites a corridor and if it's a cell on the outer bounds of the room add a new wall to the touching cell (if it's not empty) ...

So far so good, we have rooms in the map, but they cannot yet be reached because we're missing doors ...

Part 3 - part 2 - adding doors and cleaning up

You might ask why I haven't added the doors as soon as I've added the room to the dungeon (and I might just reply that I just didn't mention it), but nope, I didn't add doors - that's the next step.

The reason is quite simple, though. I don't want doors cluttered all over the space and because of that I added another (optional) thing to the room data: hasDoorInDirection ... this way we make sure that there is only one door per wall / room when we add doors ...

Yet again we loop over all rooms and over their outer bounding cells, if we touch another cell, store the current position as possible door location. Then pick a random one per direction and check if it touches another room and if this room might already have a door ...
I guess that's easier to explain with some more code:

private function createDoors (myDungeon:Dungeon, bOneDoorPerRoom:Boolean = true):void {
    
    var rnd:MersenneTwister = MersenneTwister.getInstance();
    
    var i:uint;
    var j:uint;
    
    var x:uint;
    var y:uint;

    var cellTouch:Cell;
    var myRoom:Room;
    
    var aDoor:Array;
    var pDoor:Point;
    var pNext:Point;
    
    for (i = 0; i < myDungeon.aRoom.length; i++) {
        
        myRoom = (myDungeon.aRoom[i] as Room);
        
        aDoor = [[], [], [], []];

        // collect possible door locations ...
        for (y = 0; y < myRoom.iHeight; y++) {
            for (x = 0; x < myRoom.iWidth; x++) {
                
                pDoor = new Point(myRoom.pOffset.x + x, myRoom.pOffset.y + y);
                
                if (y == 0 && pDoor.y > 0) {
                    pNext = myDungeon.getNextPos(pDoor, Dir.NORTH);
                    cellTouch = myDungeon.cell(pNext);
                    if (!cellTouch.isUnused || cellTouch.iType == RoomType.CORRIDOR) {    // the check for a cooridor is needed because they might be just one cell long ...
                        aDoor[Dir.NORTH].push(pDoor);
                        if (cellTouch.isDeadEnd) aDoor[Dir.NORTH].push(pDoor); // double chances for dead ends ...
                    }
                }
                
                if (x == (myRoom.iWidth - 1) && pDoor.x < (myDungeon.iWidth - 1)) {
                    pNext = myDungeon.getNextPos(pDoor, Dir.EAST);
                    cellTouch = myDungeon.cell(pNext);
                    if (!cellTouch.isUnused || cellTouch.iType == RoomType.CORRIDOR) {
                        aDoor[Dir.EAST].push(pDoor);
                        if (cellTouch.isDeadEnd) aDoor[Dir.EAST].push(pDoor); // double chances for dead ends ...
                    }
                }
                
                if (y == (myRoom.iHeight - 1) && pDoor.y < (myDungeon.iHeight - 1)) {
                    pNext = myDungeon.getNextPos(pDoor, Dir.SOUTH);
                    cellTouch = myDungeon.cell(pNext);
                    if (!cellTouch.isUnused || cellTouch.iType == RoomType.CORRIDOR) {
                        aDoor[Dir.SOUTH].push(pDoor);
                        if (cellTouch.isDeadEnd) aDoor[Dir.SOUTH].push(pDoor); // double chances for dead ends ...
                    }
                }
                
                if (x == 0 && pDoor.x > 0) {
                    pNext = myDungeon.getNextPos(pDoor, Dir.WEST);
                    cellTouch = myDungeon.cell(pNext);
                    if (!cellTouch.isUnused || cellTouch.iType == RoomType.CORRIDOR) {
                        aDoor[Dir.WEST].push(pDoor);
                        if (cellTouch.isDeadEnd) aDoor[Dir.WEST].push(pDoor); // double chances for dead ends ...
                    }
                }
            }
        }
        
        // now just pick one door per side ...
        for (j = 0; j < Dir.NUM_BASEDIR; j++) {
            
            if (aDoor[j].length > 0) {
                
                pDoor = aDoor[j][rnd.Range(0, (aDoor[j].length - 1))];
                pNext = myDungeon.getNextPos(pDoor, j);
                
                if (!myRoom.hasDoor(j)) {
                    myRoom.setDoor(j, pDoor);
                    
                    if (bOneDoorPerRoom && myDungeon.cell(pNext).iType == RoomType.ROOM) {
                        myDungeon.getRoom(myDungeon.cell(pNext).iValue).setDoor(Dir.getOppositeDir(j), pNext);
                    }
                    
                    myDungeon.cell(pDoor).setWall(j, WallType.DOOR);
                    myDungeon.cell(pNext).setWall(Dir.getOppositeDir(j), WallType.DOOR);
                }
            }
        }
    }
}

Viola done ... but wait one more thing, cleaning up ...

The last step might not be needed, but imho it makes some nice dungeons: after we've added all the rooms and doors, we remove all remeaning dead ends. This way there will be no corridors just ending somewhere and the map looks nicer.

So we just run the removeDeadEnds method again, this time with 100% ... now:done.

As with the last parts, the link to a working demo of the whole mess is here or here.

nGFX

#    Comments [4] |
 
Monday, June 29, 2009

Random Dynamic Levels - Part 2

This is part 2 of my collection of articles that'll deal with the theory (and the actual creation) of random dynamic levels for a (space)game. In part one we created a damn pretty maze and in part two we're going to modify it a good deal.

If you take a look at part one's output you'll notice that the code generates a pretty random maze. And there we got out first drawback: it's pretty darn random, way to random to assemble a man made structure and not quite what we would expect a spacestation / space ship to look like.

So the first modification I'm going to add will be a method that can reduce the randomness of the maze.

Part 2 - part 1 - making something not that random

My idea is to qualify the randomness by a percentage value, so a random factor of 0 will give you long straight passages that only change direction if the need to (random at that), while using a value of 100 the method will never (as far as it possible) return the same direction twice.

Of course that tiny little addition causes a lot of fuzz and requieres to rewrite a part of the core maze generator function. In part 1 I used a method to get all surrounding cells of a given point, but in order to use the direction modifier we need to use directions instead.

          //... skipped

          while (iCellCount < iTotalCells) {
                
                // get neighbor cells ...
                aCellDirections = myDungeon.getPossibleDirections(pCurrentCell);
                
                // set the cell
                if (aCellDirections.length != 0) {
                    
                    /* old way no direction modification used
                    iRndCell = rnd.Range(0, (aCellNeighbors.length - 1));
                    iRndDir = Dir.getDirFromPoint(pCurrentCell, aCellNeighbors[iRndCell]);
                    */

                    iRndDir = this.getFactoredRandomDir(iLastDir, aCellDirections, iDirChange);
                    pNextCell = myDungeon.getNextPos(pCurrentCell, iRndDir);
                    iLastDir = iRndDir;
                    
                    // remove walls
                    myDungeon.cell(pCurrentCell).setWall (iRndDir, WallType.OPEN);
                    // old way: myDungeon.cell(aCellNeighbors[iRndCell]).setWall(Dir.getOppositeDir(iRndDir), WallType.OPEN);
                    myDungeon.cell(pNextCell).setWall(Dir.getOppositeDir(iRndDir), WallType.OPEN);
                    
                    // store for later use ...
                    aCellStack.push(new Point(pCurrentCell.x, pCurrentCell.y));
                    // old way: pCurrentCell = new Point(aCellNeighbors[iRndCell].x, aCellNeighbors[iRndCell].y);
                    pCurrentCell = new Point(pNextCell.x, pNextCell.y);
                    
                    iCellCount++;
                } else {
                    pPopCell = aCellStack.pop();
                    pCurrentCell = new Point(pPopCell.x, pPopCell.y);
                }
                
            } // while

Some new variables in there: iLastDir (so we can keep track of the last direction used), pNextCell (a point that stores the next cell, basically just a temp. variable), iRndCell has been removed and aCellNeighbours has been renamed to aCellDirections ...

There are two new methdods: getPossibleDirections and getFactoredRandomDir. The first one returns an array that just contains directions that can be used (ie. cells that have not been visited yet), directions are simply stored as 0=North, 1=East and so one (I've encapsulated them into a Dir class to make it easier to read). The second method is a neat example how to make things overly complicated ...

        private function getFactoredRandomDir (iLastDir:int, aListDir:Array, iFactor:int = 50):int {
            
            var rnd:MersenneTwister = MersenneTwister.getInstance();
            var bChangeDir:Boolean = (rnd.Range(0, 99) < iFactor);
            
            var iReturn:int = iLastDir;
            
            // the last used dir is not in the list of possible new directions, so we need to pick a random one ...
            if (aListDir.toString().lastIndexOf(iLastDir.toString()) == -1) {
                iReturn = aListDir[rnd.Range(0, (aListDir.length -1))];
            } else {
                
                // we must change direction AND have at least 2 choices
                if (aListDir.length > 1) {
                    
                    if (bChangeDir) {
                        while (iReturn == iLastDir) {
                            iReturn = aListDir[rnd.Range(0, (aListDir.length -1))];
                        }
                    }
                    
                } else {
                    // just pick what's left ...
                    iReturn = aListDir[0];
                }
                
            }
            
            return iReturn;
            
            
        }


AS3 arrays (in CS3) don't have the nice method I know from c#: contains which would have been oh so easy to use here. I toyed for a fraction of a second with the idea to use a loop to check if a given value would be in an array, but then decided to go ... quick and dirty and use toString and lastIndexOf instead.

The code above is quite easy, so I only do a quick run through it...
- decide if we need to apply a direction change
- if we need to, check if the last dir is in the list of possible dirs, if not just pic a random new (this applies to both states: need to change and keep direction)
- otherwise just pick a random dir until it's not equal the last dir used

That's it.

Running the test app with different values seems to produce the desired results:
0% produces the most possible straight halls,
50% produces somewhat random halls
100% produces a maze with no straight hall at all.

Part 2 - part 2 - still way to much filled space ...

Looking at the maze reveals that there are no free spaces in it, of course we could just paint rooms over it, but I doubt it'll look like what I have in mind.
Randomly removing cells from the map is no option (even if we do check if we would just block a passage), but what about removing cells that just end the passage (ie: dead ends).
Looking at the maze again, it seems that we have (depending on the randomness of direction changes) a lot of them, so our next task would be to find those dead ends and remove them. The first "problem" that comes to me is that each time we remove dead ends, we'd create new ones. In order to clean up the map we only run the "removDeadEnds" methods a couple of times and we're done - right?

Not quite.

If we choose some unlucky values, it might happen that we kill the whole maze and that's something we don't want at all.

I decided to use a percentage of TotalCells that I want to be removed, so if we use 50%, the method should remove half of all available cells.

        public function removeDeadEnds (myDungeon:Dungeon, iRemoveDeadEnd:int = 20):Dungeon {
            
            var rnd:MersenneTwister = MersenneTwister.getInstance();
            
            var i:int;
            var j:uint;
            var iDir:int;
            var iRndCell:int;
            
            var iDeadEndsToRemove:int = Math.ceil((myDungeon.iWidth * myDungeon.iHeight) * iRemoveDeadEnd / 100);
            var iDeadEndCount:int = 0;
            
            var bExit:Boolean = false;
            
            var aTmp:Array;
            
            // the worst case may only return one dead end per run, so
            // to be sure we run it as many times as we may max need
            for (i = 0; i < iDeadEndsToRemove; i++) {
                
                aTmp = myDungeon.getDeadEnds();
                
                if (aTmp.length > 0 && !bExit) {

                    while (aTmp.length > 0) {
                    
                        // this is to make sure that the cells are somewhat even
                        // distributed if we do not use the whole lot
                        iRndCell = rnd.Range(0, (aTmp.length - 1));
                        iDir = myDungeon.cell(aTmp[iRndCell]).getDeadEndDir();
                        
                        myDungeon.cell(myDungeon.getNextPos(aTmp[iRndCell], iDir)).setWall(Dir.getOppositeDir(iDir), WallType.WALL);
                        myDungeon.cell(aTmp[iRndCell]).setWalls();
                        
                        aTmp.splice(iRndCell, 1);
                        
                        if (++iDeadEndCount >= iDeadEndsToRemove) {
                            bExit = true;
                            break;
                        }
                        
                    }
                } else {
                    break;
                }
                
            }
            
            return myDungeon;
            
        }


The comments should explain quite well what's going on in there. Only thing to mention is that I pic random dead ends if there are more available dead ends than cells to remove.

Compile and test ... and viola well done for today. :)

(I must admid it took longer to type all that than to code, so I had a bit of spare time left and coded something alse ;) )

I think that is enough for today, you can see the result (and from the upcoming articles, too) Random Dynamic Level Creation Test page (or here if the server is down).

nGFX

#    Comments [0] |
 
Wednesday, June 24, 2009

Random Dynamic Levels - Part 1

After a good while of non-techy pimpings I decided to go the x++ route and describe the process of diving into a new game ...

(CE is again on hold due to some gameplay issues I found during testplays - oh well)

There is no name to the game yet, but I can say as much as it will feature random dynamic level creation (as used in Diablo 1 for example), sci-fi themed, using RPG elements and use Unity (though the levels created will look quite different to Diablo, but the idea is the same).
I want to have random levels because that would add to the replay value of the game. I want a single game to last between 15 and 45 minutes. You should be able to start playing and kill the end-boss in that time. After that you should be able just play again, but with different set of maps ...

But to get things rolling a lot quicker I wanted to rapid prototype my ideas using flash/AS3 and then port it to c#.
The reason for this is quite simple: output. In order to "see" what the level will look like without having to worry about how to display them (ie. place 3d walls, create the assets). With using flash i can just grab the data from the generator classes and use the drawing api to quickly throw out a few lines to show the generated data.

Let's dive straight in.

Part 1 - part 1: To cell or to tile ... and is there anything we need before that?

Before I started I thought that it would be nice to move between maps (in case you thought you forgot something), to do so there needs to be a way to store the maps ... I thought of 2 methods:
a) store all visited maps
b) make them reproduceable

I prefer the later one.

So instead of using some built in random number generator I decided to use my "own" that takes a seed and the produces the same set of random numbers when using that seed - perfect.
I found some old AS1 source of some older rnd gen that I could have ported but a quick search showed that there are some more powerfull ones. After bit of research I decided to go with the Mersenne Twister algorithm. As I was too lazy to see if there was an AS3 port, I wrote my own implementation, though you could use any rnd method you want.

The next one was a bit tricky: cell based or tile based?
Tiles are easy to use and to handle, but they are limited to a single spot and mostly do only have a single "state", ie. you can walk on them or not. This might be ok in most cases but for what I have in mind they are too limited.

A cell in my case is a tile with 4 walls (north, east, south and west), if all 4 walls are set, the cell is "closed" ie. a solid rock in a clasical dungeon. The cell also stors a single integer value (so based on it's useage I could store an index in it.
Here is the code for the cell:

package com.gamingyourway.Dungeon {
    
    /**
     * Cell datatype
     * @version 2009 06 24
     * @author nGFX
     */

    public class Cell {
        
        public static const WALL:int = 0;
        public static const OPEN:int = 1;
                
        private var _aWall:Array;    // array of values ... 0=empty ...
        private var _iValue:int;    // stores a single value, ie. room num
        private var _bVisited:Boolean;    // has cell been visited
        private var _bCorridor:Boolean;    // is cell a corridor
        
        
        public function get aWall ():Array { return this._aWall; }
        public function set aWall (value:Array):void { this._aWall = value; }
        
        public function get iValue ():int { return this._iValue; }
        public function set iValue (value:int):void { this._iValue = value; }
        
        public function get bVisited ():Boolean { return this._bVisited; }
        public function set bVisited (value:Boolean):void { this._bVisited = value; }
        
        public function get bCorridor ():Boolean { return this._bCorridor; }
        public function set bCorridor (value:Boolean):void { this._bCorridor = value; }
        
        public function get wallCount ():int {
            
            var i:uint;
            var iCount:int = 0;
            
            for (i = 0; i < this._aWall.length; i++) {
                if (this._aWall[i] == 0) iCount++;
            }
            
            return iCount;
            
        }
        
        public function get isDeadEnd ():Boolean { return (this.wallCount == 3); }
        public function get isUnused ():Boolean { return (this.wallCount == 4); }
        
        /**
         * Creates a new empty (ie. all walls set) cells
         * @param    iValue    used to stor a single bit of info, ie. room num
         */

        public function Cell (iValue:int = 0) {
        
            this._aWall = [0, 0, 0, 0];
            this._iValue = iValue;
            this._bVisited = false;
            this._bCorridor = false;
            
        }
        
        /**
         * sets a single wall
         * @param    iDir    Direction of the wall
         * @param    iWall    value of the wall, 0 is a solid wall, any other value makes it "open"
         */

        public function setWall (iDir:int, iWall:int = 0):void {
            
            this._aWall[iDir] = iWall;
            
        }
        
        /**
         * return the value if the wall in iDIr
         * @param    iDir    direction of the wall to get
         * @return    value of the wall
         */

        public function getWall (iDir:int):uint {
            
            return this._aWall[iDir];
            
        }
        
        /**
         * shortcut for testing if there is a closed wall in iDir
         * @param    iDir    direction to test
         * @return    true if there is a wall
         */

        public function hasWall (iDir:int):Boolean {
            
            return (this._aWall[iDir] == 0);
            
        }
        
        /**
         * if the cell is a dead end, return the direction of the opening
         * @return    the direction of the opening
         */

        public function getDeadEndDir ():int {
            
            var iReturn:int = -1; // not a dead end
            
            if (isDeadEnd) {
                
                if (this._aWall[Dir.NORTH] != 0) iReturn = Dir.NORTH;
                if (this._aWall[Dir.EAST] != 0) iReturn = Dir.EAST;
                if (this._aWall[Dir.SOUTH] != 0) iReturn = Dir.SOUTH;
                if (this._aWall[Dir.WEST] != 0) iReturn = Dir.WEST;
                
            }
            
            return iReturn;
            
        }
        
        /**
         * returns a string representation of the cell
         * @return    a string for the falls of this cell
         */

        public function toString ():String {
            
            var i:uint;
            var strReturn:String = "";
            
            for (i = 0; i < this._aWall.length; i++) {
                strReturn += this._aWall[i].toString();
            }
            
            return strReturn;
            
        }
        
        
    }
    
}


Part 1 - part 2 - Storage and creation

To store the maps generated I wrote a simple Mapa datatype, it'll store a 2d array of cells along with some very basic methods to deal with the data.
The map type also stores width and height in an rectangle, to have an easy way to check if a point lies within the boundaries of the map.
Aditional methods so far:

hasCellInDir (pPos:Point, iDir:uint):Boolean
getNextPos (pPos:Point, iDir:uint):Point
getSurroundingCells (pPos:Point, bUsed:Boolean = false):Array


To create a map (let's call it dungeon for the sake of easiness) I didn't include the methods need to create a dungeon in the map class, instead I wrote a DungeonGenerator class, that returns a filled map class. This way I can mess around with the creation process without messing with the map class.

Part 1 - part 3 - Let's start with a simple maze ...

The most simple representation of a dungeon I can think of is a maze. Mazes are incredibly easy to create and they work oh so well with cells.

The walkthrough to create a maze:
1. create a map of "solid" cells
2. pick a random solid cell as starting point
3. get surrounding solid cells and pick a random one
4. knock the walls between these cells, store the "old" cell in a stack for later use
5. use the cell picked in 3 as new starting point and start over
6. if the current cell has no solid neighbours, pop one from the stack
7. repeat until there are no more solid cells


Easy, eh?

package com.gamingyourway.Dungeon {
    import de.drygoods.Random.MersenneTwister;
    import flash.geom.Point;
    
    /**
     * Dungeon generator
     * @version 2009 06 24
     * @author nGFX
     */
    public class DungeonGenerator {
        
        private var _Dungeon:Dungeon;
        private var _rnd:MersenneTwister;
        
        public function DungeonGenerator(iWidth:int, iHeight:int) {
            
            this._Dungeon = new Dungeon(iWidth, iHeight);
            
            this._rnd = MersenneTwister.getInstance();
            
        }
        
        public function createMaze (iDirChange:int = 100):Dungeon {
            
            var aCellStack:Array = new Array();
            var aCellNeighbors:Array;
            
            var iTotalCells:int = this._Dungeon.iWidth * this._Dungeon.iHeight;
            var iCellCount:int = 1;
            var iRndCell:int;
            var iRndDir:int;
            
            var pCurrentCell:Point = new Point(this._rnd.Range(0, this._Dungeon.iWidth -1), this._rnd.Range(0, this._Dungeon.iHeight -1));
            var pPopCell:Point;
            
            while (iCellCount < iTotalCells) {
                
                // get neighbor cells ...
                aCellNeighbors = this._Dungeon.getSurroundingCells(pCurrentCell);
                
                // set the cell
                if (aCellNeighbors.length != 0) {
                    
                    iRndCell = this._rnd.Range(0, (aCellNeighbors.length - 1));
                    iRndDir = Dir.getDirFromPoint(pCurrentCell, aCellNeighbors[iRndCell]);
                    
                    // remove walls
                    this._Dungeon.cell(pCurrentCell).setWall (iRndDir, 1);
                    this._Dungeon.cell(aCellNeighbors[iRndCell]).setWall(Dir.getOppositeDir(iRndDir), 1);
                    
                    // store for later use ...
                    aCellStack.push(new Point(pCurrentCell.x, pCurrentCell.y));
                    pCurrentCell = new Point(aCellNeighbors[iRndCell].x, aCellNeighbors[iRndCell].y);
                    
                    iCellCount++;
                } else {
                    pPopCell = aCellStack.pop();
                    pCurrentCell = new Point(pPopCell.x, pPopCell.y);
                }
                
            } // while
            
            
            return this._Dungeon;
            
        }
        
    }
    
}


The code of the dungeon generator ... for now only with the maze creation in it.
Note that the variable iDirChange is currently not used, but I'll go over it in the 2nd part of this article.

I think that is enough for today, you can see the result (and from the upcoming articles, too) Random Dynamic Level Creation Test page (or here).

See you next time when I add some direction modifications and take care of dead ends ...

nGFX


#    Comments [7] |