Last Good Quote: Son's are the seasoning on our lives. - Someone on Facebook

Showing posts with label game programming. Show all posts
Showing posts with label game programming. Show all posts

Saturday, April 19

Got Programming Skills?

Think you got programming skills?

Think you could build an app in 8 hours?

Make some money proving it....If I get enough free time, I plan to participate in one of these.

Matter of fact, if you want to do one of these I will definitely help.

http://challengepost.com/discover


Saturday, March 22

JS Starfield

The following will create a flowing field of stars on a black background. Used the HTML5 Canvas element to create this effect:

<script>

var canvas = "";
var context = "";
var stars = new Array();



function loadStarfield()
{
canvas = document.getElementById("canvas_starfield");
context = canvas.getContext("2d");

addNewStars(100);

//stars[stars.length-1].starSize = 50;
//stars[stars.length-1].drawColor = "#333";
//stars[stars.length-1].position.x = -10;
//stars[stars.length-1].position.y = 150;
//stars[stars.length-1].velocity.x = 1;


starfieldLoop();

}


function starfieldLoop()
{
context.fillStyle = "black";
context.fillRect(0, 0, canvas.width, canvas.height);

for(var i=0;i<stars.length;i++)
{
drawStar(stars[i]);
stars[i].animate();
}

window.requestAnimationFrame(starfieldLoop);

}

function drawStar(star)
{
  var position = star.position;
  context.fillStyle = star.drawColor;
  context.beginPath();
  context.arc(position.x, position.y, star.starSize, 0, Math.PI * 2);
  context.closePath();
  context.fill();
}




function addNewStars(numStars)
{
for (var i = 0; i < numStars; i++)
{
stars.push(new Star());
}
}

function Star() {
  this.position = new Vector(randBetween(0,canvas.width),randBetween(0,canvas.height)); // Vector
  this.velocity = new Vector(randBetween(1,5),0);
  var c = randBetween(0,9);
  this.drawColor = "#FFF"; //"#" + c + "" + c + "" + c; // So we can tell them apart from Fields later
  this.starSize = randBetween(1,1);
}

Star.prototype.animate = function()
{
this.position.add(this.velocity);

if(this.position.x > canvas.width)
{
this.position.x = 0;
}
}



//////////////////////////////////////////////////////////////////////////////////////////////
//
// Utils
//
//////////////////////////////////////////////////////////////////////////////////////////////
function randBetween(min, max)
{
return Math.floor(Math.random()*(max-min+1)+min);
}

function Vector(x, y) {
  this.x = x || 0;
  this.y = y || 0;
}

// Add a vector to another
Vector.prototype.add = function(vector) {
  this.x += vector.x;
  this.y += vector.y;
}

// Gets the length of the vector
Vector.prototype.getMagnitude = function () {
  return Math.sqrt(this.x * this.x + this.y * this.y);
};

// Gets the angle accounting for the quadrant we're in
Vector.prototype.getAngle = function () {
  return Math.atan2(this.y,this.x);
};

// Allows us to get a new vector from angle and magnitude
Vector.fromAngle = function (angle, magnitude) {
  return new Vector(magnitude * Math.cos(angle), magnitude * Math.sin(angle));
};

</script>

<canvas id="canvas_starfield" width="600" height="300"
style="
border:5px solid black;">
</canvas>
<script>
loadStarfield();
</script>



Friday, March 21

Interesting Map Building Links


Ray Casting in 2D Top Down Tiles
http://www.codeproject.com/Articles/15604/Ray-casting-in-a-2D-tile-based-environment

Auto Tiling/Laying Tiles Programtically/Tile Laying algoithm
http://www.saltgames.com/2010/a-bitwise-method-for-applying-tilemaps/

Other Links
http://stackoverflow.com/questions/8901987/map-tiling-algorithm




Wednesday, March 19

Articles on World Building

World Building and World Auto Generation

Simple Rogue Like -
http://kuoi.org/~kamikaze/GameDesign/art07_rogue_dungeon.php

Random 2D Tile Map Generation
http://stackoverflow.com/questions/11968167/random-2d-tile-map-generating-algorithm?rq=1


Saturday, March 1

Spawning Monsters

I've been thinking about monster spawning. For those that might now know, this means to place a new enemy creature on a map. A few thoughts I have.

As I develop the monster spawning will get progressively complex, so this is an iterative approach.

Variables:
MaxMonstersNearBy
Spawn Point - Coordinates of spawn point
Spawn Radius - As a player moves monsters will spawn within this area


  1. Check if there is more than MaxMonstersNearBy, if so exit. MaxMonstersNearBy will be set by the particular segment of the map the player is on.
  2. Check if there any players within viewing distance of the spawn point. If so exit. We don't want monsters to "pop" on the screen, players will wonder into them, or monsters will wonder into players
  3. Spawners will have a maximum number of monsters they can have spawned at any point in time.
  4. Check if there any spawn points outside of viewing distance but within SpawnRadius of the player. For each spawn point proceed.
    1. Choose a random leader, whose level is within 1 level of the player.
    2. Leaders have a number of followers that will spawn with them.
    3. Place the leader and the followers on an empty spot near the spawn point.

AI:
  1. Monsters will have one of the follow modes specified:
    1. Attack - Aggressively chase any player within x distance
    2. Guard - Stay still and attack only when attacked
    3. Wander - Moves randomly in a direction
    4. Follow - Follow a given unit, in the event that unit can not be found, start to wander.
  2. Leaders will set the mode of all followers
  3. Followers with no leader will go into wander mode.
Realistic Movement:
  1. Recall that AI processing (aiThinker) is only called once every few seconds.
  2. AI will choose 5 waypoints and move between those waypoints until the aiThinker is called again.
This is for my game Blades of War.



Saturday, November 16

Particle Emitters - Part III

And here are the actual effects themselves. It generates the actual effect:


function WhirlEmitter(point,color)
{
    this.position = point; // Vector
    this.velocity = Vector.fromAngle(0,.5); // Vector
    this.spread = Math.PI/1; // possible angles = velocity +/- spread
    this.drawColor = "#999"; // So we can tell them apart from Fields later
    this.particleColor = color;
    this.maxParticles = 100;
    this.particleSize = 1;
    this.numParticles = 0;
    this.emissionRate = 1;
    this.tether = rand(60,100);
}

WhirlEmitter.prototype.emit = function()
{
    var angle = this.velocity.getAngle() + this.spread - (Math.random() * this.spread * 2);
    var magnitude = this.velocity.getMagnitude();

    var velocity = new Vector(0,0);//Vector.fromAngle(angle, magnitude);
    var position = new Vector(this.position.x + velocity.x * 25, this.position.y + velocity.y * 25);
    var particle = new Particle(position,velocity,new Vector(0,0),this.particleColor,2);
    particle.action = "whirlParticle";
    particle.particleColor = this.particleColor;//'rgb(' + rand(0,255) + ','+ rand(0,255) + ','+ rand(0,255) + ')'
    particle.tether = rand(1,360)/100;
    particle.origin = new Vector(this.position.x, this.position.y);

    //this.position.x++;
    return particle;
};


function whirlParticle(particle)
{
    //alert(particle.life + ":" + particle.tether);
    var tmpX = particle.origin.x + particle.radius * Math.cos(particle.tether);
    var tmpY = particle.origin.y + particle.radius * Math.sin(particle.tether);
    particle.position = new Vector(tmpX, tmpY);
    particle.tether+=.25;
    particle.particleSize-=.01;
   
    if(particle.tether > 7)
    {
        particle.tether = .36;
        particle.radius-=5;
    }
   
    if(particle.radius < 0 || particle.particleSize <= 0)
    {
        return "";
    }

    return particle;
}

Saturday, November 9

Particle Emitters - Part II

Check out the Particle Emitters Series to get the full picture.

There are some basic functions that are needed for to make this work, they are as follows. We won't be modify this much except for the plotParticles function, where we have to change the if statements for each of our effects. (I know there must be a clever way to do this, but I ran outta time)


function Particle(point, velocity, acceleration,color,size) {
  this.position = point || new Vector(0, 0);
  this.velocity = velocity || new Vector(0, 0);
  this.acceleration = acceleration || new Vector(0, 0);
  this.origin = new Vector(0,0);
  this.color = color
  this.particleSize = size;
  this.action = "";
  this.life = 0;
  this.tether = 0;
  this.radius = 25;
}

function plotParticles(boundsX, boundsY)
{
    // a new array to hold particles within our bounds
    var currentParticles = [];

    for (var i = 0; i < particles.length; i++)
    {
        var particle = particles[i];

       
        var pos = particle.position;

               
        if(particle.action == "warpParticle")
        {
            particle = warpParticle(particle);
            particle.move();
        }

        if(particle.action == "whirlParticle")
        {
            particle = whirlParticle(particle);
        }
       
        if(particle.action == "beamParticle")
        {
            particle = beamParticle(particle);
        }
       
       
        if(particle != "")
        {
            // If we're out of bounds, drop this particle and move on to the next
            if (pos.x < 0 || pos.x > boundsX || pos.y < 0 || pos.y > boundsY) continue;
            if (particle.particleSize <= 0) continue;

           
            // Add this particle to the list of current particles
            currentParticles.push(particle);
        }
    }

    // Update our global particles, clearing room for old particles to be collected
    particles = currentParticles;
}



function rand(from,to)
{
    return Math.floor(Math.random()*(to-from+1)+from);
}


function drawParticles()
{
    // For each particle
    for (var i = 0; i < particles.length; i++)
    {
        context.fillStyle = particles[i].color;
        var position = particles[i].position;
        //context.fillRect(position.x, position.y, particles[i].particleSize, particles[i].particleSize);

        context.fillStyle = particles[i].particleColor;
        context.beginPath();
        context.arc(position.x, position.y, particles[i].particleSize, 0, Math.PI * 2);
        context.closePath();
        context.fill();

    }
}


function addNewParticles()
{
    for (var i = 0; i < emitters.length; i++)
    {
        if(emitters[i].numParticles < emitters[i].maxParticles)
        {
            for (var j = 0; j < emitters[i].emissionRate; j++)
            {
                particles.push(emitters[i].emit());
                emitters[i].numParticles++;
            }
        }

    }
}



Particle.prototype.move = function () {
  // Add our current acceleration to our current velocity
  this.velocity.add(this.acceleration);

  // Add our current velocity to our position
  this.position.add(this.velocity);
};



function Vector(x, y) {
  this.x = x || 0;
  this.y = y || 0;
}

// Add a vector to another
Vector.prototype.add = function(vector) {
  this.x += vector.x;
  this.y += vector.y;
}

// Gets the length of the vector
Vector.prototype.getMagnitude = function () {
  return Math.sqrt(this.x * this.x + this.y * this.y);
};

// Gets the angle accounting for the quadrant we're in
Vector.prototype.getAngle = function () {
  return Math.atan2(this.y,this.x);
};

// Allows us to get a new vector from angle and magnitude
Vector.fromAngle = function (angle, magnitude) {
  return new Vector(magnitude * Math.cos(angle), magnitude * Math.sin(angle));
};

Saturday, November 2

Particle Emitters - Part I

I've been playing around with Particle Emitters. Those are the cool little animations like star bursts and flowing bubbles that you see in games.

I used the following as a blank canvas for me to work with:

(notice you will need particleFunctions,js and particleEffects.js which are coming in part II and III)

<html>
  <head>
    <script language=javascript src="particleFunctions.js"></script>
    <script language=javascript src="particleEffects.js"></script>
  </head>
  <body>
    <canvas    style="width:300px; height:300px;border:1px solid black;background:black;" width=300 height=300></canvas>
<script>
var canvas = document.querySelector('canvas');
var context = canvas.getContext('2d');
var particles = [];
emitters = new Array();
emitters.push(new WhirlEmitter(new Vector(canvas.width/2+5,canvas.height/2),'green'));
emitters.push(new BeamEmitter(new Vector(canvas.width/2,canvas.height/2),'green'));
    
function loop() {
    clear();
    update();
    draw();
    queue();
}

function clear() {
  context.clearRect(0, 0, canvas.width, canvas.height);
}

function queue() {
  window.requestAnimationFrame(loop);
}

function update() {
  addNewParticles();
  plotParticles(canvas.width, canvas.height);
}

function draw() {
    drawParticles();
}

loop();
</script>

    </body>
</html>

Friday, October 4

Love\Hate AI

I don't normally do this, but I think that this information should not be lost:

This is a Word for Word post from: http://home.swipnet.se/dungeondweller/development/dev00055.htm


INTRODUCTION
------------
This article is based on a system proposed by Hansjoerg Malthaner
[hansjoerg.malthaner@danet.de] which in turn based some of it on Joseph Swing's
excellent article "A better monster AI". The system described here is used for
my own RL Dungeondweller.

There are many ways to create an interesting AI. One way would be to hardcode
each creatures behaviour. If enough work is put into such a hardcoded system
the results can be quite impressing, but as said, it needs a lot of work.

A better (or at least faster) way of creating an interesting AI would be to
take a look at behaviourism. Behaviourism states that a creature reacts to
external stimuli. This is not entirely true since humans have a mind and a
concept of Self, but the general idea behind behaviourism is sufficient for the
AI of a roguelike.

Imagine what would happen if you teleported a Kobold into a big dungeon. Lets
say that the Kobold starts in an empty corridor. The first thing the Kobold does
is to look around and ensure that there are no enemies in sight. If he is alone
he decides that he feels safer if he wields a sword and equips some armour. At
the moment this is his most urgent need. The Kobold reacts to the situation and
puts on his leather armour and wields his shortsword. If the Kobold still
doesn't see anyone he begins to interest himself for his environment. His most
urgent need would then be to move. The Kobold continues to move through the
corridor until he reaches a door. If the Kobold still doesn't see anyone his
most urgent need would be to open the door. The Kobold reacts to this new
situation and opens the door. He then looks around again and sees that the door
led into a big room. Near the door lies a pile of shining gold coins and at the
other end of a room a big snake looks back at the Kobold! The Kobold evaluates
this new environment, and decides that he loves the gold more than he feares and
hates the snake. The Kobold reacts to the situation and starts to move closer to
the pile of gold. As the Kobold gets closer his fear and hate of the snake grows,
as does his love for the gold. At some point the Kobolds fear is greater than the
Kobolds love. The Kobolds most urgent need will become to move away from the
snake. The Kobold reacts to this and starts to retreat from the snake. All of a
sudden a door opens to the left of the Kobold and another Kobold appears. Our
Kobold evaluates this new scenario, and immediately recognise the other Kobold as
a friend. The Kobold loves :-) other Kobolds, and this new Kobold is armed to the
teeth with shining armour and pointy weapons. Once again his love for the gold
and for his new found friend is greater than the fear of the snake. The Kobold
reacts, moves towards the gold and his friend and picks it up. ...

The story above seems fairly realistic and it is not that difficult to achieve
a behaviour similar to the behaviour of the Kobold. It all boils down to a three
step process:

1.Observe and evaluate the environment
2.Find the most urgent need, based on the environment and the self
3.Perform the need

First the AI need to look around and evaluate it's environment, just like the
Kobold did. The AI assigns values for Fear, Love and Hate to all creatures and
objects within sight. The next step is to find the most urgent need at that
time. The AI base this descision on the Fear, Love and Hate for perceived
objects and creatures and on its own status (the Kobold puts on equipment). When
the most urgent need is found it is performed, and the cycle starts over again.

Each of the above steps are explained in detail below:



1 OBSERVE AND EVALUATE THE ENVIRONMENT
--------------------------------------
Before the AI decides what to do it needs to evaluate its environment. The AI
need to look around and memorize all objects and creatures that it can perceive.
This can be done using normal LOS, or any other suitable way. Each object or
creature (from now on entity) perceived is assigned a Fear/Love value (1.2) and
a Hate value (1.3). The most Feared, Loved and Hated entity is memorized (1.4)
and the Fear/Love and Hate center is calculated (1.5). These values will be used
later on to determine where to move or whom to attack.

How to achieve all this? Well, it is fairly simple. To help us with our
calculations we need a couple of help methods:


1.1 Help methods
----------------
These methods are made to make life easier for us during the calculation of the
Fear/Love and Hate values. The methods could have more uses in your game, so
they are worth implementing. Each entity needs a three methods called;
physicalCondition(), perceivedStrength() and perceivedDanger().


1.1.1 physicalCondition()
-------------------------
this method returns the entity's current hitpoints divided by its maximum
hitpoints. This gives us a value from 0 and upwards:

0 - Dead
<1 -="" 1="" damaged="" not="">1 - Boosted


1.1.2 perceivedStrength()
-------------------------
This method should return the entity's perceived strength, ie how strong it
looks if we were to see it. This value should be based on the entity's physique,
not powerful equipment. I base the value on the entity's strength multiplied by
it's physicalCondition(). You may wish to include more factors when calculating
the perceived strength, but this works fine.


1.1.3 perceivedDanger()
-----------------------
This method should return the entity's perceived danger, ie how dangerous it
looks. This value should be based on the entity's equipment. If the entity has
sharp weapons and heavy armour the returned value should be high.


Ok, now that we have some methods to work with, we can assign Fear/Love and Hate
values to the perceived entities.


1.2 The Fear/Love value
-----------------------
The Fear/love value, assigned to each perceived entity, is based on factors
such as the perceived entity's strength, danger and distance from the AI. If
the entity is a friend it gets a positive value (=love) and if it is an enemy
it gets a negative value (=fear). The closer to the Ai, the higher the value.

The AI rates a friend/enemy differently if the AI belongs to an instinct
driven creature (tiger, snake etc), a creature with a mind (human, orc etc) or
a mindless creature (undead).

1.2.1 Mindless Fear/Love
------------------------
A mindless creature doesn't have any feelings at all and rates friends and
enemies as Fear/Love = 0

1.2.2 Instinctdriven Fear/Love
------------------------------
A creature driven by instincts doesn't have the intelligence to measure a
friends equipment. It measures a friend by how much stronger he/she is than
itself:

(1000 * (friend->perceivedStrength() / own->perceivedStrength())) / distance

Enemies are measured in the same manner (except for the '-'):

(-1000 * (enemy->perceivedStrength() / own->perceivedStrength())) / distance


1.2.3 Minddriven Fear/Love
--------------------------
A creature with intelligence measures a friend both by its strength and by its
equipment. If the friend is stronger and/or better equipped it is loved more:

(500 * (friend->perceivedStrength() / own->perceivedStrength()) +
500 * (friend->perceivedDanger() / own->perceivedDanger())) / distance

And enemies:

(-500 * (friend->perceivedStrength() / own->perceivedStrength()) +
-500 * (friend->perceivedDanger() / own->perceivedDanger())) / distance

You may wish to change the 500 * strength and 500 * danger to other values. Some
minddriven creatures may value equipment more than strength and vice versa.
Remember that the sum should be 1000. There is no particular reason for choosing
1000, you could go with even higher numbers to achieve higher variation, but I
think 1000 is enough.


Remember that Fear/Love also applies to objects. If the perceived entity is an
object you may wish to assign a value to it. Treasures and similar items would
become Loved, at least by most minddriven creatures. Similarly some kinds of
objects might be Feared (unholy symbols etc)


1.3 The Hate value
------------------
The Hate value is based on the AI's feeling for other entities along with the
distance from the AI. The more it hates something the higher the value. As for
the Fear/Love value you can differentiate between instinct, mind and mindless
AI. You can also add extra rules to make your AI hate some creatures more than
others. For simplicity I just set the rule that if the AI isn't of the same
race as the perceived entity the Hate value is set to 1000 / distance,
otherwise 0.



1.4 Memorize most Feared, Loved and Hated
-----------------------------------------
You need to keep track of the most Feared, Loved and Hated entity that the AI
can perceive. This is just a matter of saving the currently most Feared, Loved
and Hated creatures Fear/Love or Hate value along with a pointer or similar.
This is kind of implementation specific, so just solve it in a way suitable for
your game. It should present no problem.


1.5 Calculating the Fear/Love and Hate center
---------------------------------------------
This is where it gets really interesting. Here you need to calculate where the
Fear/Love and Hate center are positioned among all of the perceived entities.
I'll try to illustrate what we wish to achieve:


.........  In this first example the AI (A) can perceive only one entity (1)
.1.......
.........  The center of Fear/Love and Hate are then positioned on the same
.........  cell as the entity
.........
....A....
.........

.........  In this example the AI (A) can perceive two entities (1) and (2)
.1..x..2.  The AI has assigned exactly the same Fear/Love and Hate values to
.........  both entities
.........
.........  The center of Fear/Love and Hate are then positioned at a cell on
....A....  equal distance from both (1) and (2). In this case cell (x)
.........


.........  In this example the AI (A) can perceive two entities (1) and (2)
.1f.h..2.  The AI has assigned a higher Fear/Love value to (1), but the same
.........  Hate values to both (1) and (2)
.........
.........  The center of Fear/Love (f) is then positioned closer to (1) than
....A....  than (2). The center of Hate (h) is still placed at equal distance
.........  from (1) and (2)


.........  As you add more entities the Fear/Love and Hate centers are moved
.1.....2.  around more and more.
.........
...h.....
.........
....A....
..f......
.........
.3.......


At a first glance this may seem quite complex, but it is fairly simple to
calculate. The first thing you need to keep track of is the total Fear/Love and
Hate values for the entities that you have assigned Fear/Love and Hate values
to. The next thing you need is a x,y coordinate for the Fear/Love and Hate
center.

When the AI begins to evaluate its environment the total Fear/Love and Hate are
zero and the Fear/Love center and Hate center has not been assigned any values.

After assigning a Fear/Love and Hate value to the first of the perceived
entities the center of Fear/Love and Hate are positioned at the same coordinates
as the entity, just like in the first figure above.

Subsequent position calculations are done like this:

1.Find the smallest of absolute value of total Fear/Love and absolute value of
perceived entity's Fear/Love. ie:

  minFL = __min(abs(totalFearLove),abs(entityFearLove))

2.Find the largest, ie:

  maxFL = __max(abs(totalFearLove),abs(entityFearLove))

3.Subtract the x coordinate of the Fear/Love center from the perceived entity's
x coordinate, and multiply the result by (minFL / maxFl). Add the resulting
value to the Fear/Love center's x coordinate. ie:

  fearLoveX += ((entityX - fearLoveX) * (minFL / maxFL))

4.Do the same for y. ie:

  fearLoveY += ((entityY - fearLoveY) * (minFL / maxFL))

5.Repeat the same procedure for Hate center

  minH = __min(totalHate,entityHate)    absolute values not nescessary since
  maxH = __max(totalHate,entityHate)    Hate value always is positive
  hateX += ((entityX - hateX) * (minH / maxH))
  hateY += ((entityY - hateY) * (minH / maxH))


After you have repeated all of the above for all of the AI's perceived entities
you know which entity the AI Feares, Loves and Hates the most. You know of the
total Fear/Love and Hate values for all of the perceived entities, and you know
where the center of Fear/Love and Hate are positioned.

When all of this is done we have evaluated the environment, and have all the
knowledge we need to decide for an appropriate course of action.



2 FIND THE MOST URGENT NEED (CONTESTING ACTIONS)
------------------------------------------------
Each AI driven creature needs to be assigned a set of actions that it can
perform. The actions vary depending on the type of creature. A humanoid should
be able to perform actions such as OPEN, PICKUP, DROP, EQUIP, MOVE and ATTACK
while a snake is a bit more limited; MOVE and ATTACK.

Each action is represented by two methods called rate() and execute(). The
methods take a reference to the creature in question as parameter.

Each time the AI need to decide which action to perform, the rate() method is
called on each of the available actions. The rate() method decides how urgent it
is to perform the action. A high value gives the action high priority and a low
value gives the action low priority. The action that has the highest rating is
the one being performed, i.e calling the execute() method.

This is simple enough, but how to rate each action? This is the tricky part, and
it needs a bit of balancing before it works. I used fixed values for the
priorites:

PRIORITY_NONE    0
PRIORITY_LOW     50
PRIORITY_NORMAL  100
PRIORITY_HIGH    200
PRIORITY_HIGHEST 800

Let's have a look at some of the actions one by one:

2.1 OPEN.rate(creature*)
------------------------
The method examines the mapcells surrounding the creature. If a closed door is
found the method returns a priority of NORMAL, otherwise 0. The closed door is
remebered so that the execute method knows what to open.


2.2 PICKLOCK.rate(creature*)
----------------------------
The method examines the mapcells surrounding the creature. If a locked door is
found the method returns a priority of NORMAL, otherwise 0. The locked door is
remebered so that the execute method knows what to lockpick.


2.3 DROP.rate(creature*)
------------------------
In the system that I use for Dungeondweller, each creature has a method called
physcialSpeed(). The method returns a value from 0 and upwards, 0 meaning
immobilized, less than 1 is slowed, 1 normal speed and greater than 1 meaning
fast. The method takes into account the creatures strength, the amount of
inventory carried and the creatures physical condition.

If the creature is slowed down (physicalSpeed() < 1) the rate() method decides for an object to drop, and returns a priority of NORMAL/physicalSpeed(), otherwise rate() returns 0. This gives a higher value the more encumbered and wounded the creature is. If extremely hurt the creature will throw away inventory to be able to flee faster.  There is a lot of room for intelligent selection of object to drop, but you could just start from the top of the creature's inventory, or choose the heaviest object in the inventory.   2.4 PICKUP.rate(creature*) -------------------------- If the creature is standing on an object and it isn't slowed down the method returns a priority of NORMAL, otherwise 0. The object is remebered so that the execute() method knows what to pick up.  You might wish to modify the priority if the creature is a scavenger or if the object to pick up is valueable etc.   2.5 EQUIP.rate(creature*) ------------------------- If the creature isn't equipped with a weapon and it has a weapon in its inventory the method returns a priority of HIGH, otherwise it continues to check for other things to equip (armour, rings etc). If something is found it is remembered for the execute() method and the priority is HIGH. If nothing needs to be equipped the method returns 0.  You could expand this and check for better equipment in the inventory than the already equipped equipment. Checks for broken weapons and similar things can also be done here.   2.6 MOVE.rate(creature*) ------------------------ If and where to move the creature is decided by the environment. This is where all of our calculations in step 1 comes into use.  If the total Hate and total FearLove are zero the creature should just move around in a manner that seems intelligent. There are many different ways to move a creature on a map. This is a rather big topic and it would be more appropriate to address this in another article. You could just write something simple that made the creature follow the walls or similar. Remember the direction of movement and return with a priority of LOW  Else if the total Hate is zero, and total Fear/Love is non zero the creature should move towards the Fear/Love center. This will make the creature move towards it's friends. To prevent you creatures from forming big packs that won't move an inch since the all love eachover that much you could also check the creatures distance to the Fear/Love center. If the creature is within a certain distance, say three cells, just move the creature using the same system as for total Hate and total Fear/Love equal to zero. Remember the direction of movement and return a priority of NORMAL  Else if the total Fear/Love is bigger than the total Hate the creature is at a scary place and should flee. The creature should move in a direction directly away from the Fear/Love center. Remember the direction of movement and return a priority of HIGHEST.  Else the creature should move to attack, and direction of movement should be towards the center of Hate. Remeber the direction of movement and return a priority of HIGH.   2.7 ATTACK.rate(creature*) -------------------------- Last but not least we have the attack action. If the most Hated entity is within range of any of the creatures attacks the target is remembered and the method returns a priority of HIGHEST.    3 Perform the need ------------------ All that is left now is to call the execute() method of the action that returned the highest rating. I kept the code for the execute methods very general so I could use the same execute() code when the player performs his/her actions.    4 How to achieve more variation ------------------------------- At the moment I have only implemented the basic system described above, but from the discussion at r.g.r.d Hansjoerg suggested the following tweaks:  How to get variety into the AI? Of course we can give each monster individual fears and hates. We can change the things/monsters which are feared and hated and we can change the strength of fear and hate. This will make cowards and fighters from our monsters. The love value (a monster has negative fear for friends) will make them move towards each other until their fear reduces and if they hate something nearby they will move there as a pack.  More variety can be introduced by restriction the monsters perception. Some monsters may see far others not. Some may regocnize wepaons as dangerous some not. If a monster does not recognize wepons it might not fear a heavily armed player and attackk him (stupid monster). Another monster might fear the player if it preceives him healthy and well armed. It fears hime less if wouned.  Such a monster will stay away from the player as long as he is well and wields his weapon. This monster will attack if the players weapon breaks or the player self is wounded ("Clever monster").  If the mosnters fear drop faster with distance than the hate, such a monster will follow the player, because it fears to be near the player, but stil hates him from a distance, which makes it stay away a certain distance.  Monsters with ranged attacks which fear the player will stay away from him, until their fear drops, and then stay there and attack the player with their ranged attacks.  If fear is made to depend on the health of the monster, an monster could rush towards the player (no fear) and attack him. After the first strike they receive the fear goes up and the monster runs away. This would yield the above mentioned "hit-and-run" tactic.  From kamikaze to cowards quite some different monsters seem to be able to be modeled by this simple system. I'm not sure if it will really work until I made my tests. One thing is sure: monster stats must hold all the mentioned information (what is feared, what is loved, what is hated, how strong does fear drop with distance, how strong does hate drop with distance, can the monster perceive weapons and armor, how tough is the monster/how strong does it fear to be wounded)   Send any comments, questions or suggestions to: Björn Bergström dungeondweller@swipnet.se 


Wednesday, September 4

Sharing Game Info Across Sites

The idea of sharing characters, objects and user information across multiple games and environments has always interested me.

I've got a simplistic idea on how this could be done and wanted to share it.

Some assumptions:
Let's assume we have 3 environments (games) Game A, B and C. They are similar in some concepts and vastly different in others.

Let's assume we have one user, Joe, who wants to play in all three environments.

Step 1:

Joe signs up for Game A and plays it. All information is stored within Game A as normal.

When Joe logs out of Game A. He is given a url that will contain all his information.
www.GameA.com/userInfo.php?userID=10

Next Step:

Joe now wants to play Game B. He has the option of entering in his url or registering as normal.

Once given a url Game B will request Joe's information from Game A.

Game A will response with pair values, like so:
UserName=Joe
Email=joe@somewhere.com
Score=1000
Money=500
CharacterName=Killer
CharacterHP=50
CharacterMagic=40
etc...
etc...
Game A will also record the fact that Game B requested information. This will come into play when Joe goes back to playing Game A.

Game B will take the values it can use and create/update Joe's account within it's game environment. Such as UserName, Score and Money. (Maybe this game does not have characters)

Next Step:

Joe is done playing Game B. As he logs out he is given the opportunity to get a new URL or he can update Game A.

If Joe decided to update Game A, Game B will send a request to Game A letting it know the url it can find an update at.
www.GameB.com/userInfo.php?userID=99

Game A will go to that url and process all of Joe's information, updating it's stats as needed.

Thoughts:

One of the advantages of this system is that if Site A were to disappear or be unavailable, Joe still retains all the information from Game B. It's even possible for him to save information from Game A, if the site does disappear, if Game B has it saved.

For those who are worried about malicious use of the system, it would be quite easy to code the update process to only allow updates from certain game sites. If you did not want to see updates from Game C, you could exclude that site from your updates by looking at the request url.

Alternatively someone could offer a service where "trusted" games can exchange information.

The user information could be in an XML document, but I have found that the ease with which you can build key pair values is easier to build and search then XML documents. I know the advantages, I personally don't think the advantages out weigh the disadvantages in this case.

This is a stab in the dark any thoughts and comments are appreciated.

Thursday, January 27

More about Jarvis the Chat Bot

I mentioned that I created a chat bot named Jarvis. Since then I've been asked a few times what goes into creating Jarvis. I thought I'd take the time and explain what is going on behind the scenes.

First off Jarvis sits on top of database with the following tables.

  • word - A library of known words he found online
  • keywords - collections of words that Jarvis thought went together
  • responses - Response that can be given to a user
There are other tables, but that is the core ones.

Now I loaded Jarvis with the library that ELIZA the original chat bot used. That system had a number of keywords and responses to those key words. For example: If the user had typed "Hi", there were 3 possible responses. "How are you?", "Hello", "Welcome".

Jarvis would randomly pick one of those.

I also hand entered in some other "choice" keywords and phrases.

The next step was to have him learn from input. So when ever Jarvis puts out a response that includes a question (question mark), he would wait for the response and tag it as a possible "answer" to that question. That way when another user asked the question of Jarvis this was in his possible responses.

So for example, Jarvis might start off with a memory like the following:

Key: How are you?
Possible Responses: "Good", "I'm sad" or "I'm happy".

Let's say he had the following conversation with user 1.

Jarvis: Hi
User 1: Hello Jarvis
Jarvis: How are you?
User 1: I'm good, I had bacon this morning.


Now Jarvis' memory looks like the following:

Key: How are you?
Possible Responses: "Good", "I'm sad" or "I'm happy" or "I'm good, I had bacon this morning"


So when user 3 comes along the conversation looks like this...

Jarvis: Hello
User 3: Hi Jarvis, How are you?
Jarvis: I'm good, I had bacon this morning
User 3: That sounds good
Jarvis: how are you?
User 3: Hungry


Jarvis used the first users answers and he also learned a new answer to the question "How are you?"

Key: How are you?
Possible Responses: "Good", "I'm sad" or "I'm happy" or "I'm good, I had bacon this morning", "Hungry"


Curiosity
At first I built him so that any response that was a question was given priority. But that lead to Jarvis asking to many questions. So Jarvis now has a curiosity factor. The more curious he is, the more likey that he will give a response that is in the form of a question. This sponsors more responses for him.

You can increase or decrease curiosity by typing "moreCurious" or "lessCurious" into the text box.

Peeking into Jarvis Brain
There is a checkbox "Peek into Brain". Clicking this will let you see into Jarvis process. It shows you the following:

  • Words gathered from your input
  • Last 5 words learned (popularity is in parens)
  • The Keywords or phrases that were triggered based on your words
  • Selected/Best Keyword is highlights in bold
  • The Responses triggered from the best keyword
  • The last 5 learned keyword phrases
  • The last 5 learned responses
Scoreing
Jarvis tracks each chat session and gives it a score. Keep in mind each possible response is scored. Anytime Jarvis uses an response with a score higher than 0, he adds one to the session score.

This should lead to interesting sessions having a higher score, but I have not thoroughly tested that.

Jarvis can be found at http://www.mobeamer.com/lab/jarvis

Randomness

There are two places where Jarvis uses randomness.

First: Sometimes after finding all the keyphrases from the users input. Multiple keypharases have the same score. In this case, Jarvis will randomly select one of them.

Second: Sometimes after finding all responses from a keyphrase, mulitple responses have the same score. In this case, Jarvis will randomly select one.

The random function uses php's in-built rand() function. Which is not 100% random but close enough for me.

Monday, January 24

Jarvis - A chatterbot

If you didn't know AI (artificial intelligence) has become a bit of a hobby of mine. In an effort to explore the domain, I thought that I would throw together a little chat bot.

He's based off of the original chat bot, ELIZA.

I have/had a lot of thoughts of the direction I could take this in, but I wanted to get a version 1.0 all wrapped up and released before taking on other challenges.

You can chat with Jarvis at: www.mobeamer.com/lab/jarvis/

Some fun facts:
  • Jarvis was named after the AI in Iron Man
  • My 11 year old son, taught Jarvis a few key sayings
  • Sometimes Jarvis is incredibly stupid
  • Jarvis has a fully built out relationship model for words, key phrases and responses.
  • Jarvis can learn from chatting with other users
Some thoughts on what to implement next:
  • Allow Jarvis to "read" or learn from websites (Wikipedia)
  • Allow Jarvis to formulate higher "concepts" where responses are all connected via a single word or key phrase.
  • Allows users to chat with other users and Jarvis at the same time and ask them to spot Jarvis in the crowd
  • Allow Jarvis to get happier the longer someone chats with him
  • Add some type of visual representation of Jarvis

Friday, April 23

Javascript - 2D Map Library

This article is a wrap up of a series of articles in which I walk through building a simple 2d top down map library. Below are the articles that build this library:
For those of you that want the code, scroll past the explanations, it's at the bottom.

Style.css.
While testing and playing I usually will include the style in one page, for the ease of editing, but it's always a good idea to separate the style sheet from the main body of code. This allows an artist to come along and play with the look and feel of the site without screwing with your brilliantly created code.

Map.js.
All the general re-usable functions for mapping. If you ever finish coding a function and think, man I'll never have to touch this again, its so perfect and I'll use it all the time! First, slap yourself, cause we all know it's going to have a bug in it at some time. Second realize that the function in question, is prime material to go into a library of sorts. The functions in map.js are generic and can be kept from game to game as you build different things. At least that is my hope.

TileClicked()
I left tile clicked in the main body of the page because the actions you might want to take when a tile is clicked will change from game to game. Thus it is not library material.

DestReached()
This is a new function that was not discussed in other articles. Once a unit is done moving to it's destination of location, this function is called. I figured game builders might want to change this function depending on the game.

Play()
This is the heart of your game, if you want enemies to run around randomly, here is where you put that stuff. This function will call itself every few milliseconds, the more code in this function the slower your game is going to run.

ObjectsCollided()
This function is called when ever two units combine. The unit that moved last and did the collision is passed along with the id of each unit that is involved in the collision. You'll want to customize this function.


The Body Tag
The body tag calls the buildMap and addUnit function. I'd expect that you will want to change that.

Div Elements
There are two divs in the html one to hold the map and another to hold the score. You can move those around, but they must be somewhere in the page for the library to work.

Map.js Function List

BuildMap(width, height)
Builds a map of width and height. It will also add walls to all edges of the map, so units don't wonder off it. Comment out the "addBorders()" line if you don't want this.

AddBorders()
Ummm, it adds borders...nuff said

addWall(x,y)
Turns tile x,y into a wall. Sets tile's flag = -1, sets the background color

addUnit(unitID, x, y)
Adds a unit with that ID to the map at tile location x, y. Multiple units can be added. If unitID is 1000 it is given the css class "unit". Otherwise the class is "enemy"

setWalkableFlag(x, y, flag)
Sets tile x, y "flag" variable to the flag passed.

jumpUnit(unitID, tileDestX, tileDestY)
Moves a unit directly to that tile, no animation at all...unit just jumps to location.

move(unitID, dir)
Move a unit one tile in the direction specified. Valid directions are N, E, S, W. This is an animated move (one pixel at a time). This uses the moveUnit() function.

moveUnit(unitID, tileDestX, tileDestY)
Moves a unit to the tile passed in. If at anytime the unit hits a none walkable tile, the unit will move back to last position. (no route finding). Calls DestReached() when completed.

isWalkable(x,y)
Returns "Y" or "N" if pixel x, y is walkable

isWalkableTile(x,y)
Returns "Y" or "N" if tile x, y is walkable

getTop(obj)
Return the top pixel (y) of the object. Could use some optimization

getLeft(obj)
Return the left pixel (x) of the object. Could use some optimization

translateTileX(x), translateTileY(y)
return the tileX that pixel x is located within

getTileX(unitID), getTileY(unitID)
return the tile x and tile y that the unit is located on

Here be code!

Style Sheet (style.css)


.tile {
background:#BBB;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.unit {
background:green;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.enemy {
background:red;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.food {
background:black;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}



Map.js (the library)



var originX = 0;
var originY = 0;
var tileSize = 16;
var mapHeight = 0;
var mapWidth =0;
var score=0;
var playSpeed = 1000;
var numUnits = 0;
var maxNumUnits = 40;


function buildMap(width, height)
{
mapHeight = height;
mapWidth = width;

for(x=0;x<width;x++)
for(y=0;y<height;y++)
{
var d = document.createElement("DIV");
d.className="tile";
d.setAttribute("ID","tile_" + x + "_" + y);
d.style.top = originY + (y*tileSize);
d.style.left = originX + (x*tileSize);
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("flag","0");
d.setAttribute("onclick","tileClicked(" + x + "," + y + ")")
document.getElementById("divMap").appendChild(d);
}

addBorders();



}

function addBorders()
{
for(x=0;x<mapWidth;x++)
{
addWall(x, 0);
addWall(x, mapHeight-1);
}


for(y=0;y<mapHeight;y++)
{
addWall(0, y);
addWall(mapWidth-1,y);
}
}

function addWall(x,y)
{
var tile = document.getElementById("tile_" + x + "_" + y)
tile.setAttribute("flag","-1");
tile.style.background = "#777"
}


function addUnit(unitID, x,y)
{
var d = document.createElement("DIV");
var tile = document.getElementById("tile_" + x + "_" + y)
if(unitID == 1000)
{
d.className="unit";
}
else
{
d.className="enemy";
}
d.setAttribute("xVel", 0);
d.setAttribute("yVel", 0);
d.setAttribute("ID","unit_" + unitID);
d.style.top = tile.style.top;
d.style.left = tile.style.left;
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("currTileX",x);
d.setAttribute("currTileY",y);
d.setAttribute("priorTileX",x);
d.setAttribute("priorTileY",y);

setWalkableFlag(x, y, unitID);

document.getElementById("divMap").appendChild(d);
}

function setWalkableFlag(x, y, flag)
{
var tmpTile = document.getElementById("tile_" + x + "_" + y)
tmpTile.setAttribute("flag", flag);

}

function jumpUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
unit.style.left = getLeft(tile);
unit.style.top = getTop(tile);
//
//mark the current tile as UNwalkable
//
setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), "0");

//
//mark the old tile as WALKABLE
//
setWalkableFlag(unit.getAttribute("priorTileX"), unit.getAttribute("priorTileY"), "0");

unit.setAttribute("currTileX",x);
unit.setAttribute("currTileY",y);
unit.setAttribute("priorTileX",x);
unit.setAttribute("priorTileY",y);

}



function move(unitID, dir)
{
x = getTileX(unitID)
y = getTileY(unitID)

if(dir == "N")
{
y--;
}

if(dir == "E")
{
x++;
}

if(dir == "S")
{
y++;
}

if(dir == "W")
{
x--;
}


if(isWalkableTile(x, y) == "Y")
{
moveUnit(unitID, x, y);
}


}


function moveUnit(unitID, tileDestX, tileDestY)
{

var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
unit.setAttribute("priorTileX",getTileX(unitID));
unit.setAttribute("priorTileY",getTileY(unitID));

if(!tile)
{
alert("no tile found");
return;
}
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile);
destY = getTop(tile);
newX = x;
newY = y;

if(x > destX) newX--;
if(x < destX) newX++;
if(y > destY) newY--;
if(y < destY) newY++;

if(isWalkable(newX, newY) == "Y" && isWalkable(newX+tileSize-2, newY + tileSize-2) == "Y")
{
unit.style.left = newX;
unit.style.top = newY;

if(newX != destX || newY != destY)
{
setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5);
}
else
{
destReached(unitID, destX, destY);
}
}

}

function isWalkable(x,y)
{
var tile = document.getElementById("tile_" + translateTileX(x) + "_" + translateTileY(y))
if(tile.getAttribute("flag") >= 0) return "Y";


return "N";
}


function isWalkableTile(x,y)
{
var tile = document.getElementById("tile_" + x + "_" + y)
if(tile.getAttribute("flag") >= 0) return "Y";


return "N";
}

function getTop(obj)
{
if(obj)
{
return parseInt(obj.style.top.replace("px",""));
}

return 0;
}

function getLeft(obj)
{
if(obj)
{
return parseInt(obj.style.left.replace("px",""));
}

return 0;
}


function translateTileX(x)
{
return Math.floor(x/tileSize - originX);
}

function translateTileY(y)
{
return Math.floor(y/tileSize - originY);
}


function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getTop(unit));
}

function getTileX(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getLeft(unit));
}

function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
x = Math.floor(getLeft(unit)/tileSize - originX);
y = Math.floor(getTop(unit)/tileSize - originY);

return y;
}




Basic HTML page (index.html)


<html>
<link type="text/css" href="style.css"rel="stylesheet" />
<script language=javascript src="map.js"></script>

<script language=javascript>
function tileClicked(x,y)
{
//called whenever a tile is clicked
moveUnit(1000, x, y);
}

function destReached(unitID, x, y)
{
//called when ever the unit is done moving to a spot

}


function play()
{
//once called, this will loop forever, checking the game state and other things
setTimeout("play()",playSpeed);
}


function objectsCollided(unit, unitID, enemyID)
{
//called whenever two objects collide

}

</script>

<body onload="buildMap(10,10); addUnit(1000,4,4);">

<div id="divMap"></div>

<div id="score" style="font-family:verdana;size:14px; background:green;position:absolute;top:5px;left:350px; width:100px height:15px;border:5px solid green;">
Score: 0
</div>


</body>
</html>

Monday, April 5

Code: Simple 2D Javascript Map - Part 6

This code will create a map grid, when you click the grid the green box will move to the tile you clicked. Red boxes will "fall" from the top down at random intervals. If the green box hits a red box, the red box disappears and starts to fall again. Think of it as a catching game.

This was built to demonstrate one form of collision detection.In my mind there are two ways you can do collision detection. The first method is what most systems and algorithms perform:
  • Calculate the new position of the unit
  • Cycle through all other units to see if one of them occupies the new position
  • If no one is occupying then move the unit

The second method is to make tiles as walkable as a unit steps on the tile. Then rather then cycling through all the units, you just check the tile the unit is moving to.

  • Calculate the new position of the unit
  • Check to see if that tile is occupied
  • If no one is occupying then move the unit

The second method means that it "cost" more to move a unit. However it cost less than cycling through all units, every time there is any movement on the screen.

How did I implement? First, I had to edit the buildMap() function so that each tile has a flag for who is standing on it. So I added the following line inside my buildMap() loops:

d.setAttribute("flag","0");


I then needed a function to set and unset this flag. If you un comment out a few lines, it will change the background of the tile, so that you can see which tiles get marked as unwalkable.

function setWalkableFlag(x, y, flag)
{
var tmpTile = document.getElementById("tile_" + x + "_" + y)
tmpTile.setAttribute("flag", flag);

/*
if(flag > 0)
{
tmpTile.style.background = "yellow";
}
else
{
tmpTile.style.background = "#BBB";
}
*/
}


I also need the unit to "remember" what tile they are on (currTile) and what tile they were on (priorTile). To do this, I add the following code to the addUnit() function:


d.setAttribute("currTileX",x);
d.setAttribute("currTileY",y);
d.setAttribute("priorTileX",x);
d.setAttribute("priorTileY",y);
setWalkableFlag(x, y, unitID);


Again, I needed a few "wrapper" functions. One translates a pixel point X to a tile X. To keep things clean I changed the getTile function to use the new translate function. Again, there is plenty of room to optimize.


function translateTileX(x)
{
return Math.floor(x/tileSize - originX);
}

function translateTileY(y)
{
return Math.floor(y/tileSize - originY);
}


function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getTop(unit));
}

function getTileX(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getLeft(unit));
}


Now we get to the meat and potatoes of coding. The move unit function is where all the collision detection takes place. Reading through the logic of this function reads as follows:
  1. Grab the unit object and the destination tile object
  2. If the destination tile does not exist, stop
  3. Move 1 pixel closer to the destination
  4. Get the tile located at the center of the new position. (Center of the unit)
  5. If that tile is not flagged or if it is flagged as this unit
  6. Move the unit to the new location
  7. Set the priorTile and currentTile values
  8. Flag the new current tile as walkable
  9. Flag the old prior tile as unwalkable
  10. If we still have not found our destination, repeat in 5 milliseconds
  11. If there is a collision (back at #5), call the collide function



function moveUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
unit.setAttribute("priorTileX",getTileX(unitID));
unit.setAttribute("priorTileY",getTileY(unitID));

if(!tile)
{
return;
}
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile);
destY = getTop(tile);
newX = x;
newY = y;

if(x > destX) newX--;
if(x < destX) newX++; if(y > destY) newY--;
if(y < destY) newY++; checkX = newX + tileSize/2; checkY = newY + tileSize/2; var newTile = document.getElementById("tile_" + translateTileX(checkX) + "_" + translateTileY(checkY)) if(newTile.getAttribute("flag") == "0" || newTile.getAttribute("flag") == unitID) { unit.style.left = newX; unit.style.top = newY; unit.setAttribute("currTileX",getTileX(unitID)); unit.setAttribute("currTileY",getTileY(unitID)); if(unit.getAttribute("priorTileY") != unit.getAttribute("currTileY") || unit.getAttribute("priorTileX") != unit.getAttribute("currTileX") ) { // //mark the current tile as UNwalkable // setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), unitID); // //mark the old tile as WALKABLE // setWalkableFlag(unit.getAttribute("priorTileX"), unit.getAttribute("priorTileY"), "0"); } if(newX != destX || newY != destY) { setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5); } } else { // //we collided //snap back to last tile // objectsCollided(unit, unitID, newTile.getAttribute("flag")); } }


In our game, if the user/player manages to collide with an enemy we will pop the enemy back up to the top for them to start over.



function objectsCollided(unit, unitID, enemyID)
{
var tmpTile = document.getElementById("tile_" + unit.getAttribute("priorTileX") + "_" + unit.getAttribute("priorTileY"))
unit.style.top = getTop(tmpTile);
unit.style.left = getLeft(tmpTile);

if(unitID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(enemyID, x, y);
}

if(enemyID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(unitID, x, y);
}

}



One last change, I had to make was in the jumpUnit() function. I found that I needed to flag and unflag the tiles as the unit jumped.

function jumpUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
unit.style.left = getLeft(tile);
unit.style.top = getTop(tile);
//
//mark the current tile as UNwalkable
//
setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), "0");

//
//mark the old tile as WALKABLE
//
setWalkableFlag(unit.getAttribute("priorTileX"), unit.getAttribute("priorTileY"), "0");

unit.setAttribute("currTileX",x);
unit.setAttribute("currTileY",y);
unit.setAttribute("priorTileX",x);
unit.setAttribute("priorTileY",y);

}


And that's the entire change I made...the following is the entire code set:

<html>
<style>
.tile {
background:#BBB;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.unit {
background:green;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.enemy {
background:red;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}
</style>
<script language=javascript>

var originX = 0;
var originY = 0;
var tileSize = 32;
var mapHeight = 0;
var mapWidth =0;
function buildMap(width, height)
{
mapHeight = height;
mapWidth = width;

for(x=0;x<width;x++)
for(y=0;y<height;y++)
{
var d = document.createElement("DIV");
d.className="tile";
d.setAttribute("ID","tile_" + x + "_" + y);
d.style.top = originY + (y*tileSize);
d.style.left = originX + (x*tileSize);
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("flag","0");
d.setAttribute("onclick","tileClicked(" + x + "," + y + ")")
document.getElementById("divMap").appendChild(d);
}
}

function tileClicked(x,y)
{
//var tile = document.getElementById("tile_" + x + "_" + y)
//tile.style.background = "yellow"

moveUnit(1000, x, y);
}

function addUnit(unitID, x,y)
{
var d = document.createElement("DIV");
var tile = document.getElementById("tile_" + x + "_" + y)
if(unitID == 1000)
{
d.className="unit";
}
else
{
d.className="enemy";
}
d.setAttribute("xVel", 0);
d.setAttribute("yVel", 0);
d.setAttribute("ID","unit_" + unitID);
d.style.top = tile.style.top;
d.style.left = tile.style.left;
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("currTileX",x);
d.setAttribute("currTileY",y);
d.setAttribute("priorTileX",x);
d.setAttribute("priorTileY",y);

setWalkableFlag(x, y, unitID);

document.getElementById("divMap").appendChild(d);
}

function setWalkableFlag(x, y, flag)
{
var tmpTile = document.getElementById("tile_" + x + "_" + y)
tmpTile.setAttribute("flag", flag);

/*
if(flag > 0)
{
tmpTile.style.background = "yellow";
}
else
{
tmpTile.style.background = "#BBB";
}
*/
}

function jumpUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
unit.style.left = getLeft(tile);
unit.style.top = getTop(tile);
//
//mark the current tile as UNwalkable
//
setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), "0");

//
//mark the old tile as WALKABLE
//
setWalkableFlag(unit.getAttribute("priorTileX"), unit.getAttribute("priorTileY"), "0");

unit.setAttribute("currTileX",x);
unit.setAttribute("currTileY",y);
unit.setAttribute("priorTileX",x);
unit.setAttribute("priorTileY",y);

}



function moveUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
unit.setAttribute("priorTileX",getTileX(unitID));
unit.setAttribute("priorTileY",getTileY(unitID));

if(!tile)
{
return;
}
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile);
destY = getTop(tile);
newX = x;
newY = y;

if(x > destX) newX--;
if(x < destX) newX++;
if(y > destY) newY--;
if(y < destY) newY++;

//if(newX < 0) newX = 0;
//if(newX > mapWidth-1) newX = mapWidth-1;
//if(newY < 0) newY = 0;
//if(newY > mapHeight-1) newY = mapHeight-1;

checkX = newX + tileSize/2;
checkY = newY + tileSize/2;

var newTile = document.getElementById("tile_" + translateTileX(checkX) + "_" + translateTileY(checkY))
if(newTile.getAttribute("flag") == "0" || newTile.getAttribute("flag") == unitID)
{
unit.style.left = newX;
unit.style.top = newY;

unit.setAttribute("currTileX",getTileX(unitID));
unit.setAttribute("currTileY",getTileY(unitID));

if(unit.getAttribute("priorTileY") != unit.getAttribute("currTileY") || unit.getAttribute("priorTileX") != unit.getAttribute("currTileX") )
{
//
//mark the current tile as UNwalkable
//
setWalkableFlag(unit.getAttribute("currTileX"), unit.getAttribute("currTileY"), unitID);

//
//mark the old tile as WALKABLE
//
setWalkableFlag(unit.getAttribute("priorTileX"), unit.getAttribute("priorTileY"), "0");
}

if(newX != destX || newY != destY)
{
setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5);
}
}
else
{
//
//we collided
//snap back to last tile
//
objectsCollided(unit, unitID, newTile.getAttribute("flag"));


}



}


function objectsCollided(unit, unitID, enemyID)
{
var tmpTile = document.getElementById("tile_" + unit.getAttribute("priorTileX") + "_" + unit.getAttribute("priorTileY"))
unit.style.top = getTop(tmpTile);
unit.style.left = getLeft(tmpTile);

if(unitID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(enemyID, x, y);
}

if(enemyID == 1000)
{
x = Math.floor(Math.random()*9);
y = 0;
jumpUnit(unitID, x, y);
}

}


function getTop(obj)
{
if(obj)
{
return parseInt(obj.style.top.replace("px",""));
}

return 0;
}

function getLeft(obj)
{
if(obj)
{
return parseInt(obj.style.left.replace("px",""));
}

return 0;
}


function translateTileX(x)
{
return Math.floor(x/tileSize - originX);
}

function translateTileY(y)
{
return Math.floor(y/tileSize - originY);
}


function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getTop(unit));
}

function getTileX(unitID)
{
var unit = document.getElementById("unit_" + unitID);
return translateTileY(getLeft(unit));
}

function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
x = Math.floor(getLeft(unit)/tileSize - originX);
y = Math.floor(getTop(unit)/tileSize - originY);

return y;
}


var playSpeed = 1000;
var numUnits = 0;
var maxNumUnits = 5;
function play()
{
if(numUnits < maxNumUnits)
{
unitID = 1001+numUnits
x = Math.floor(Math.random()*9);
speed = Math.floor(Math.random()*3);
addUnit(unitID,x,0);
var unit = document.getElementById("unit_" + unitID);
unit.setAttribute("xVel", 0);
unit.setAttribute("yVel", speed+1);
numUnits++;
}

for(i=0;i<numUnits;i++)
{
unitID = 1001+i;
var unit = document.getElementById("unit_" + unitID);
x = getTileX(unitID) + parseInt(unit.getAttribute("xVel"));
y = getTileY(unitID) + parseInt(unit.getAttribute("yVel"));

if(y >= mapHeight)
{
x = Math.floor(Math.random()*9);
y=0;
jumpUnit(unitID, x, y);
}
moveUnit(1001+i, x,y);
}

setTimeout("play()",playSpeed);
}


</script>
<body onload="buildMap(10,10); addUnit(1000,4,4);play();">

<div id="divMap"></div>

</body>
</html>


Question: How would you go about adding a score indicator?

Code: Simple 2D Javascript Map - Part 5

This code will show how to create enemies and move them around the screen. It's going to randomly create 5 red boxes which will "fall" from the top of the grid to the bottom. The user can click a tile and the green box will move to the tile clicked. There is no collision detection...yet. I'm keeping this as simple as possible so in this example, "enemies" will drop from the top of the screen. Your job is to dodge them.

The play function will get called every one second (playSpeed). It will add a new enemy until it has added 5 enemies. It chooses a random location at the top of the map, places the unit there.

It will then pick a random speed and set the y velocity (think downward speed) equal to this random speed value.

The second piece has the magic. It cycles through all enemy units and sets their next location to 3 spaces below where the unit currently is. Notice that this where our new functions get called.

var playSpeed = 1000;
var numUnits = 0;
var maxNumUnits = 5;
function play()
{
if(numUnits < unitid =" 1001+numUnits" x =" Math.floor(Math.random()*9);" speed =" Math.floor(Math.random()*3);" unit =" document.getElementById(" i="0;i= mapHeight)
{
x = Math.floor(Math.random()*9);
y=0;
jumpUnit(unitID, x, y);
}
moveUnit(1001+i, x,y);
}

setTimeout("play()",playSpeed);
}


Other changes



Again, we have some plain Jane functions to get the tile location (x and y) of where our units is located. Make a note to yourself, there is plenty of room to optimize this code. It's dog slow.


function getTileX(unitID)
{
var unit = document.getElementById("unit_" + unitID);
x = getLeft(unit)/tileSize - originX;
y = getTop(unit)/tileSize - originY;

return x;
}

function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
x = getLeft(unit)/tileSize - originX;
y = getTop(unit)/tileSize - originY;

return y;
}


We also need to create a style for our enemy units. So I added this to the style.

.enemy {
background:red;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}


Since we are moving random objects around, we need to start tracking the map height and width. So I changed the build map function.

var mapHeight = 0;
var mapWidth =0;
function buildMap(width, height)
{
mapHeight = height;
mapWidth = width;

for(x=0;x<width;x++) y=0;y<height;y++);x++)


Again, because we are moving enemies around randomly, need a bit of error catching added to the move function. If we try to move to a tile that does not exist, just stop moving.

function moveUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile)
......


Finally, I needed a function which will take an existing unit and "jump" them to a specific place on the map. This means they don't "move" one pixel at a time, but instead are immediately placed at that location:

function jumpUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
unit.style.left = getLeft(tile);
unit.style.top = getTop(tile);
}


Last but not least, we have to call our new play function from within the body tag.

<body onload="buildMap(10,10); addUnit(1000,4,4); play()">





Finally, the entire page looks like this:


<html>
<style>
.tile {
background:#BBB;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.unit {
background:green;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.enemy {
background:red;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}
</style>
<script language=javascript>

var originX = 0;
var originY = 0;
var tileSize = 32;
var mapHeight = 0;
var mapWidth =0;
function buildMap(width, height)
{
mapHeight = height;
mapWidth = width;

for(x=0;x<width;x++)
for(y=0;y<height;y++)
{
var d = document.createElement("DIV");
d.className="tile";
d.setAttribute("ID","tile_" + x + "_" + y);
d.style.top = originY + (y*tileSize);
d.style.left = originX + (x*tileSize);
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("onclick","tileClicked(" + x + "," + y + ")")
document.getElementById("divMap").appendChild(d);
}
}

function tileClicked(x,y)
{
//var tile = document.getElementById("tile_" + x + "_" + y)
//tile.style.background = "yellow"

moveUnit(1000, x, y);
}

function addUnit(unitID, x,y)
{
var d = document.createElement("DIV");
var tile = document.getElementById("tile_" + x + "_" + y)
if(unitID == 1000)
{
d.className="unit";
}
else
{
d.className="enemy";
}
d.setAttribute("xVel", 0);
d.setAttribute("yVel", 0);
d.setAttribute("ID","unit_" + unitID);
d.style.top = tile.style.top;
d.style.left = tile.style.left;
d.style.width = tileSize;
d.style.height = tileSize;
document.getElementById("divMap").appendChild(d);
}


function jumpUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
unit.style.left = getLeft(tile);
unit.style.top = getTop(tile);
}



function moveUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
if(!tile)
{
return;
}
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile);
destY = getTop(tile);
newX = x;
newY = y;

if(x > destX) newX--;
if(x < destX) newX++;
if(y > destY) newY--;
if(y < destY) newY++;

unit.style.left = newX;
unit.style.top = newY;

if(newX != destX || newY != destY)
{
setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5);
}

}


function getTop(obj)
{
if(obj)
{
return parseInt(obj.style.top.replace("px",""));
}

return 0;
}

function getLeft(obj)
{
if(obj)
{
return parseInt(obj.style.left.replace("px",""));
}

return 0;
}

function getTileX(unitID)
{
var unit = document.getElementById("unit_" + unitID);
x = getLeft(unit)/tileSize - originX;
y = getTop(unit)/tileSize - originY;

return x;
}

function getTileY(unitID)
{
var unit = document.getElementById("unit_" + unitID);
x = getLeft(unit)/tileSize - originX;
y = getTop(unit)/tileSize - originY;

return y;
}


var playSpeed = 1000;
var numUnits = 0;
var maxNumUnits = 5;
function play()
{
if(numUnits < maxNumUnits)
{
unitID = 1001+numUnits
x = Math.floor(Math.random()*9);
speed = Math.floor(Math.random()*3);
addUnit(unitID,x,0);
var unit = document.getElementById("unit_" + unitID);
unit.setAttribute("xVel", 0);
unit.setAttribute("yVel", speed+1);
numUnits++;
}

for(i=0;i<numUnits;i++)
{
unitID = 1001+i;
var unit = document.getElementById("unit_" + unitID);
x = getTileX(unitID) + parseInt(unit.getAttribute("xVel"));
y = getTileY(unitID) + parseInt(unit.getAttribute("yVel"));

if(y >= mapHeight)
{
x = Math.floor(Math.random()*9);
y=0;
jumpUnit(unitID, x, y);
}
moveUnit(1001+i, x,y);
}

setTimeout("play()",playSpeed);
}


</script>
<body onload="buildMap(10,10); addUnit(1000,4,4); play()">

<div id="divMap"></div>

</body>
</html>

Friday, April 2

Code: Simple 2D Javascript Map - Part 4

What's that you say, you want to move the character around the screen.

OK, ok, ok...here we go.

Because we will need to get the exact position of the div objects, I use the following two functions (In all fairness there are better, faster, more accurate ways to get the position of a div tag, but this has worked for me and is simple enough for my simple mind to grasp.)

You pass them an object and they give you back the position of the object.


function getTop(obj)
{
if(obj)
{
return parseInt(obj.style.top.replace("px",""));
}

return 0;
}

function getLeft(obj)
{
if(obj)
{
return parseInt(obj.style.left.replace("px",""));
}

return 0;
}



A bit boring, nothing to fascinating in those functions. However here is where things get a bit more exciting. The following function will move any unit object to the desired Tile position.

We pass in the unitID we want to move, and the TILE x and y we want to move to. The function will:
  • Get the unit's and the tile's position
  • Compare the two positions and move the unit closer to the tile by one pixel.
  • At the very end of the function, if we have not reached our destination, we call the same function in 5 milliseconds.



function moveUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile);
destY = getTop(tile);
newX = x;
newY = y;



if(x > destX) newX--;
if(x <> destY) newY--;
if(y < left =" newX;" top =" newY;">

Now that moves the unit around, but no one is actually calling this function so



Full Code:

<html>
<style>
.tile {
background:#BBB;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}

.unit {
background:green;
border:1px solid #999;
position:absolute;
top:0px;left:0px;
width:5px;height:5px;
overflow:hidden
}
</style>
<script language=javascript>

var originX = 0;
var originY = 0;
var tileSize = 32;

function buildMap(width, height)
{
for(x=0;x<width;x++)
for(y=0;y<height;y++)
{
var d = document.createElement("DIV");
d.className="tile";
d.setAttribute("ID","tile_" + x + "_" + y);
d.style.top = originY + (y*tileSize);
d.style.left = originX + (x*tileSize);
d.style.width = tileSize;
d.style.height = tileSize;
d.setAttribute("onclick","tileClicked(" + x + "," + y + ")")
document.getElementById("divMap").appendChild(d);
}
}

function tileClicked(x,y)
{
//var tile = document.getElementById("tile_" + x + "_" + y)
//tile.style.background = "yellow"

moveUnit(1000, x, y);
}

function addUnit(unitID, x,y)
{
var d = document.createElement("DIV");
var tile = document.getElementById("tile_" + x + "_" + y)
d.className="unit";
d.setAttribute("ID","unit_" + unitID);
d.style.top = tile.style.top;
d.style.left = tile.style.left;
d.style.width = tileSize;
d.style.height = tileSize;
document.getElementById("divMap").appendChild(d);
}

function moveUnit(unitID, tileDestX, tileDestY)
{
var unit = document.getElementById("unit_" + unitID);
var tile = document.getElementById("tile_" + tileDestX + "_" + tileDestY)
x = getLeft(unit);
y = getTop(unit);
destX = getLeft(tile);
destY = getTop(tile);
newX = x;
newY = y;



if(x > destX) newX--;
if(x < destX) newX++;
if(y > destY) newY--;
if(y < destY) newY++;

unit.style.left = newX;
unit.style.top = newY;

if(newX != destX || newY != destY)
{
setTimeout("moveUnit(" + unitID + "," + tileDestX +"," + tileDestY+")",5);
}

}


function getTop(obj)
{
if(obj)
{
return parseInt(obj.style.top.replace("px",""));
}

return 0;
}

function getLeft(obj)
{
if(obj)
{
return parseInt(obj.style.left.replace("px",""));
}

return 0;
}


</script>
<body onload="buildMap(10,10); addUnit(1000,4,4)">

<div id="divMap"></div>

</body>
</html>



For giggles, I used this quick function to test play it. The function adds a number of units and randomly moves them around the screen. My desktop (Intel 2 Duo 2.2GH with 2GB Ram) can handle about 20 units in firefox, before it gets a bit odd.


var playSpeed = 3000;
var numUnits = 0;
var maxNumUnits = 5;
function play()
{
while(numUnits < i="0;i

Followers