Tuesday, July 14, 2009
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

Tuesday, July 14, 2009 11:56:39 AM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [4]  |  Trackback
 Monday, July 13, 2009
I just needed to do some eye-candy the other day as a hour break from the current project, so I came up with this ribbon effect

ribbon_grab.png

Originally I did it with sprites and masks but it proved to be running too slow on most peoples machines, so a quick hack later, and it's using copyPixels and running a lot quicker and I managed to make the ribbon itself smoother.
A fairly simple old school effect, but it does look kinda cool.

Check it out here.

Squize.

Monday, July 13, 2009 8:51:34 AM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Thursday, July 09, 2009
"Be as select in those you endeavour to please, as in those whom you endeavor to imitate. Without love of fame you cannot do anything excellent"

Sir Joshua Reynolds 1772.

cronusx.jpg

X++, with the new name of cronusX, is live on Candystand.com today.

I'd like to thank everyone involved in it, especially Olli and all of you who took the time to provide great feedback here during it's development.

Enjoy the destruction

Squize.

 | 
Thursday, July 09, 2009 2:24:31 PM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [5]  |  Trackback
 Wednesday, July 08, 2009
9thJuly.png

...tomorrow

Wednesday, July 08, 2009 3:45:05 PM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, July 03, 2009
This time it's one of ours! Seems like forever since we've had something of our own to talk about ( There will be another game on the 9th of July to mention too, but more of that nearer the time ).

AQI Zoneout is the fella this time.

zoneOut_grab.jpg



It's a really quite good puzzle game that I worked on for our friends at theBasement.tv ( Check their demo reel out, seeing that really made me want to work with those guys )

It was a slightly weird development process for me, as I broke off before the game was actually finished for various asset related reasons, so that in combination with it already been planned out really well beforehand stops it feeling like it's mine, it's more something I worked on rather than made, if that makes sense.

As a project though, it was a real joy and a real pleasure to get to work with ickydime on a game together, you couldn't ask for a better go-between at a company, and the work that was done to the game after it left my sticky fingers has really raised it up, there's a lot of love in there.

I was asked as part of this pimp to also mention Miles design who were the agency for the project, Fat Atom who did the sweet php magic so you can see your name on the high-score table and of course cleanairmatters.org who are the end client.

That's more than enough words, give it a go, it'll help eat into your Friday before home time, hurray!

[ Update, it's now on Kong and NG. So if you don't want to play it on it's nice custom page, go to either site and look at the ads whilst you play ]

Squize.

Friday, July 03, 2009 2:51:33 PM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Wednesday, July 01, 2009
I've found myself defending oop recently, and at the same time bemoaning the lack of good oop tutorials with regards game development.

Rather than promising to write a full blown tutorial, as to be honest there's no way I'll stick to it, after around 3 parts at most I'll be bored stupid, I thought I'd try and explain the structure behind an existing game which we've already posted the source to. So it may be an idea to open this link in it's own tab.

Ready ? Ok, let's go.

opCFlow.png


Look at that, made with a free package so you get the watermark, nothing's too cheap for you dear reader.

This at heart is how I've structured all my games since starting with as2. It only varies in that there are sometimes more classes in there, the actual hierarchy never changes.

Ok lots of code here is never that great to read, but there's no way around it, sorry,

package {
    import Classes.Init;
    
    import flash.display.DisplayObject;
    import flash.display.Sprite;
    import flash.display.Stage;
    import flash.display.StageQuality;
    import flash.display.StageScaleMode;
    import flash.events.Event;

    [SWF(width="600", height="400", frameRate="35", backgroundColor="#000000")]

    public class Main extends Sprite{

//---------------------------------------------------------------------------------------
// Properties
//---------------------------------------------------------------------------------------
        private static var instance:Main;
        private static var __parent:DisplayObject;
        private static var stage:Stage;
        private static var init:Init;
        
//---------------------------------------------------------------------------------------
// Static ( Singleton )
//---------------------------------------------------------------------------------------
        public function Main(){
            if(instance){
                throw new Error( "Singleton and can only be accessed through Singleton.getInstance()" );
            } else {
                instance=this;
            }        

            instance=this;
//When we are running from our preloader, comment this out
            waiting();
        }

//---------------------------------------------------------------------------------------
// Public
//---------------------------------------------------------------------------------------
        public override function toString():String {
            return "Main";
        }        

//---------------------------------------------------------------------------------------
        public function waiting():void{
            addEventListener(Event.ADDED_TO_STAGE,mainAddedToStage);
        }

//---------------------------------------------------------------------------------------
// Getters
//---------------------------------------------------------------------------------------
        public static function getInstance():Main {
            return instance;
        }

//----------------------------------------------------------------------
        public function getMainMovie():DisplayObject{
            return __parent;    
        }

//----------------------------------------------------------------------
        public function getStage():Stage{
            return stage;    
        }

//----------------------------------------------------------------------
        public function getInit():Init{
            return init;    
        }

//---------------------------------------------------------------------------------------
// Private
//---------------------------------------------------------------------------------------
        private function mainAddedToStage(e:Event):void{
            stage=this.stage;
            stage.showDefaultContextMenu=false;
            stage.scaleMode = StageScaleMode.NO_SCALE;
            stage.quality=StageQuality.LOW;
            stage.stageFocusRect=false;

            __parent=this.root;
            
            init=new Init();
        }


//---------------------------------------------------------------------------------------
    }
}

Hopefully this should be fairly straight forward ( I did actually grab it from somewhere else when looking for an as3 version which matched what I was doing in as2, so sorry original author, I can't tell anymore what's yours and what's mine ). We're basically waiting around until this instance is added to the stage ( If you try and read some properties before it's technically been created then you're going to get errors. ) and then running the mainAddedToStage method. Importantly we have some getter methods set up here, as we need them ( More later ). This is our document class, and as such it has a "direct" link with the swf ( For want of a better term ). From now on throughout every other class, any reference to stage or Main is obtained from this class.
The very last line you'll see we create a new instance of the Init class ( init=new Init(); ), so let's check that class out.

package Classes {
    import flash.display.DisplayObject;
    import flash.display.Sprite;
    import flash.display.Stage;

    public class Init {
//---------------------------------------------------------------------------------------
// Properties
//---------------------------------------------------------------------------------------
        private var gameController:GameController;
        private var attract:Attract;
        private var soundHandler:SoundHandler;
        
//------------------------------------------------
// System
//------------------------------------------------
        private var main:Main;
        private var mainMovie:DisplayObject;
        private var stage:Stage;
        
//---------------------------------------------------------------------------------------
//Constructor
//---------------------------------------------------------------------------------------
        public function Init(){
            main=Main.getInstance();
            mainMovie=main.getMainMovie();
            stage=main.getStage();

            soundHandler=new SoundHandler();
           
            gameController=new GameController();
            attract=new Attract();
        }

//---------------------------------------------------------------------------------------
// Public
//---------------------------------------------------------------------------------------
        public function toString():String {
            return "Init";
        }        

//---------------------------------------------------------------------------------------
// Getters
//---------------------------------------------------------------------------------------
        public function getAttract():Attract{
            return attract;
        }

//---------------------------------------------------------------------------------------
        public function getGameController():GameController{
            return gameController;
        }

//---------------------------------------------------------------------------------------
        public function getSoundHandler():SoundHandler{
            return soundHandler;
        }

//---------------------------------------------------------------------------------------
    }
}

This again is a bit of a nothingy class. In theory it could be shoved into the Main class with no real harm, the reason I don't is pure lazyness. I like being able to copy the Main class to every new project without having to think about it, i.e aside from the [swf] tag I never have to alter it. It just feels cleaner having that first class do very little.

If you look at our Init class you'll see we just create instances of each class we need ( Refer to our cheapo diagram to help clarify ), and set up some getter methods again ( Just to clarify, a method is exactly the same as a Function, it's just that if you use a function in oop it's called a method instead. I don't know why either ), so other classes can get a reference to these instance's if they need to.

How do I decide which classes sit on which row of the hierarchy ? Main is our document class, so he's at the top and doesn't really do much. Init is Main's love child, and doesn't do much either, he's just creating the actual game classes.
The next row is more interesting. Attract is our front-end. It deals with the title screen, the game over screen, hi-score entry if the game supports it, instructions etc. Basically everything that happens before starting the game and after the game has finished.
Let's jump over to SoundHandler. This is just what it says. It's here because it's used by both Attract and GameController, as both need to have sounds. GameController is a simple class like Init, it handles the creation of all the child classes needed for the game ( Check the diagram ).

Let me try and rationalise why Attract, GameController, SoundHandler are all on the same row, in effect why they're equal to each other. We've already established that Attract and GameController need to be aware of the SoundHandler class as both need sounds. In the grand scheme of things, the Attract class doesn't really need to know about GameController ( Remember Attract is everything but the actual game ) and conversely the GameController really doesn't give a crap about Attract. If one of these classes is working, then the other isn't doing anything, so it makes sense to me that they're on the same row.
Does that mean they never need to chat to each other ? No, but it's at very key points. For example, when the player presses the "Play Game" button in Attract, we kill off all the Attract mode things and then call startGame() method in GameController.

Now we come to something cool. Quite a few times I've seen people moving over to oop, and it's all going well, until they need a reference to a different class. How the hell do we get the reference we need ? No one wants to pass a reference to a class via a constructor, as that's just messy and nasty and really easy to screw up, and it's not like we can be old school and use _root.myMethod(); when we're oop gods.

Notice all my constructors are the same, eg,

//------------------------------------------------
// System
//------------------------------------------------
        private var main:Main;
        private var mainMovie:DisplayObject;
        private var stage:Stage;
        
//---------------------------------------------------------------------------------------
//Constructor
//---------------------------------------------------------------------------------------
        public function GameController(){
            main=Main.getInstance();
            mainMovie=main.getMainMovie();
            stage=main.getStage();
        }

We get a reference to our document class, Main and store it in our local var main. We also store away a reference to our mainMovie ( To be honest I don't think I've used that ref. in as3 yet, it's just a throw back to my as2 code, but I'm a creature of habit so it'll stay there 'til as4 ) and the Stage.

So this is the GameController class, and it's game over, so we want to call the Attract class as he deals with that. This is how we get our reference:

main.getInit().getAttract().gameOver();

Check back with our Main class code up there, see getInit() in the getters section ? It returns a reference to our Init instance. Then look at the Init getters, getAttract() gives us our instance of Attract, and from there we're just calling the method gameOver in the Attract class.
Don't worry if this doesn't make sense straight away, it's quite a bit to get your head around in one go. Basically so long as we have a reference to Main in any class, we can get a reference to any other class no matter how far away from the callee class we are. References aren't a pain in the arse anymore, they're as simple as this.

You may be thinking, how the hell am I going to remember all those getOurInstanceName() method calls. Auto-completion. Flex does all this for you so it's never an issue ( And I'm sure Flash Develop does the same ). I don't know about the Flash IDE, I really don't hold much faith in it doing it, so now may be the time to install Flash Develop and have a play with that.

See we've got the basics of oop hierarchy covered, and I've not had to slip into terms like singleton and composition. Feel free to google both terms, see what they actually mean, and hopefully when you read the flowery descriptions you'll realise that you already get them as we've covered them here.

As always feel free to fire over questions or point out glaring errors in what I've done. Like I said all my games pretty much follow this pattern, which makes sharing code between projects a lot easier plus I'm not having to think about how classes are inter-connected. I'm not suggesting you just blindly copy what I've done, but perhaps plan out your next game using some simple flowchart boxes so you can see where classes should be connected and where it'll be a waste of time to do so.

Squize.

Wednesday, July 01, 2009 1:27:56 AM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [12]  |  Trackback
 Tuesday, June 30, 2009
Our mate Chris has been working away on his first Unity3D powered iPhone game for a little while, and now it's available for free on the iPhone store his mind turned to pimping, and our's to getting an interview so we could pad things out a little without just looking cheap.

snowball_title_screen.png

What I do love is the way Chris just ignores the shit in my questions and just answers the core point without rising to my bait, like I'm 12 and best just ignored.

"How did you find moving from Flash to Unity ? They seem to share a common core, but are different enough to make life interesting. How was it for you ( Darling ) ?"

In some ways it hardly felt different at all, as if they were from the same software family, Unity's version of Javascript is so close to Actionscript (for example when working on the Mac now, I even use Unity's code editor 'Unitron' for my actionscript coding) but when it came to structuring the game it really is very different.
Actually building game mechanics, levels, controls etc is really very intuitive in Unity, however there doesn't seem to be any one agreed way on storing things like player data, global game settings.
The way I ended up doing it all is with a 'gameObject' that doesn't get destroyed when moving between scenes (but this
in itself causes problems when testing then, as you don't have to test from the opening scene, and hence the gameObject hasn't been made yet.)
If someone knows a better way way please do tell me  :)
"iPhone dev via Unity, sex or a drunken wank ( Maybe with tears. Why did she leave, why ? )"

Considering what it does I really don't see how it could be any easier. It gets slightly complicated when you finally move the project over to Xcode, but then Xcode is complicated and that's nowt to do with Unity, is it wrong of me to think that maybe Apple have purposefully made this bit hard to keep the kids out?
It really is very complicated and parts of it would try the patience of a Saint, but as I said this isn't anything to do with Unity.
Maybe someone out there can let me know, is it always this convoluted when dev-ing for consoles? Are there just always weird things you have to do due to copy protection / code signing?

snowball_dev_shot.jpg


"Tell us about going through Apples hoops to get the game on the store, was it just like a great big hug, or more a spit in the eye ?"

It wasn't as bad as I thought it would be truth be told. From first submission to being live in the App Store took around 14 days. We had one
build sent back to us, as we weren't making it clear that the high score table was storing the user's data remotely and also we hadn't specifically requested the users permission to access the internet.
One amazing achievement is though that we have not received one crash report yet, which is testament to how awesome I really am (or that maybe I am working on a lovely high level piece of industry quality middleware with some brilliant engineers...hmm it's probably my awesomeness now that I think about it.)

IMG_0004.PNG

"It's early days yet, but how's the game doing ? Any sort of trend apparent or is it getting lost in the zillion new releases every day ?"

It's done well for what it is, which is a first game, proof of concept. It spent around one week in the top 30 free arcade games and is now in the top 60 or so. It's been installed around 12,000 times and we've had some lovely reviews off people (many of whom commented that is it better and easier to control the Super Monkey Ball on the iPhone).
One interesting point is that we may have got more installs had we charged. This is pure speculation on my behalf, but something I didn't realise is that many of the very popular review sites and magazines for iPhone simply won't cover free games, so even by charging only 59p or something we could conceivably got in pocketGamer, Edge, RetroGamer etc.  So I guess we will be testing my theory on this for
Snowball's Chance in Hell 2  :)

"If badgers had guns, do you think they'd rob post offices ?"

No they'd rob Mash Potato factories.

I hadn't even considered that, damn he's on intellectual fire.

Now you're wet for the game, here's the all important link http://bit.ly/kill5Snowball

Never one to miss the chance to spread the word Chris told me about Kill5's competition. Let's face it, it's not a competition, it's a bribe, but fuck it, who wouldn't want an iPod Touch ?
Read all about it here, http://www.kill5.com/competition/ but come on, I've kinda earned the iPod with this article, so really don't expect to win.
( What should happen if by some fluke I do win ? Everyone is going to think we're big cheaty cheats, I've screwed myself now haven't I ).

A big thanks to Chris for taking the time to do this interview. I'm sure if anyone has some follow up questions he'll be around to tackle them in the comments.

Squize.

Tuesday, June 30, 2009 1:03:16 PM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Monday, June 29, 2009
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

Monday, June 29, 2009 2:34:28 PM (W. Europe Daylight Time, UTC+02:00)  #    Disclaimer  |  Comments [0]  |  Trackback