[Help]2D tile map lighting

Hello, I’m currently making a game. I’m using a tile map and am trying to get lighting and shadows working for it.
It currently looks like this -

And the bit which controls the lighting is the following class -

package kingdom.blue;
 
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import javax.imageio.ImageIO;
import java.awt.Color;

 
public final class map{
    
        public static final int WIDTH = 100;
        public static final int HEIGHT = 12;
        public static final int MAPLEVEL = 2;
        public static final int TILE_SIZE_X = 50;
        public static final int TILE_SIZE_Y = 50;
        public BufferedImage[][] data = new BufferedImage[WIDTH][HEIGHT];
        public Color [] [] lightMap = new Color [WIDTH][HEIGHT];
        public LoadMapImages lmi;
        public int mapPosX;
        public int mapPosY;
        public double round = 0.5;
        boolean mapLoaded;
        
        public void renderMap(){
          for(int x = 0;x < WIDTH;x++){
            for(int y = 0;y < HEIGHT;y++){
                data[x][y] = lmi.ground5;
               data[7][HEIGHT-8] = lmi.ground3;
               data[8][HEIGHT-8] = lmi.ground1;
               data[9][HEIGHT-8] = lmi.ground2;
               data[x][HEIGHT-1] = lmi.ground1;
               data[6][HEIGHT-4] = lmi.ground2;
               data[5][HEIGHT-4] = lmi.ground3;
               data[12][HEIGHT-8] = lmi.ground2;
               data[11][HEIGHT-8] = lmi.ground3;
               //data[15][HEIGHT-4] = lmi.ground2;
               //data[14][HEIGHT-4] = lmi.ground3;
               
               data[21][HEIGHT-4] = lmi.ground2;
               
               data[20][HEIGHT-4] = lmi.ground3;
               data[24][HEIGHT-6] = lmi.ground2;
               data[23][HEIGHT-6] = lmi.ground3;
               
               data[30][HEIGHT-3] = lmi.ground2;
               data[29][HEIGHT-3] = lmi.ground3;
               
               //data[21][HEIGHT-11] = lmi.ground1;
               data[21][HEIGHT-9] = lmi.ground1;
               //data[22][HEIGHT-11] = lmi.ground1;
               data[22][HEIGHT-9] = lmi.ground1;
               //data[20][HEIGHT-11] = lmi.ground1;
               data[20][HEIGHT-9] = lmi.ground1;
               //data[20][HEIGHT-10] = lmi.ground1;
               //data[22][HEIGHT-10] = lmi.ground1;
               data[21][HEIGHT-10] = lmi.torch;
               
               
               //data[24][HEIGHT-7] = lmi.torch;
               lightMap [x][y] = new Color(0,0,0,200);
            }
          }
        }
        
        public map(Player player){
           lmi = new LoadMapImages();
        }  
        public void paint(Graphics2D g)
        {          
                //g.setColor(Color.GRAY);
                for (int x = 0; x < WIDTH; x++){
                        for (int y = 0; y < HEIGHT; y++)
                        {
                            g.setColor(lightMap[x][y]);
                            g.drawImage(data[x][y],mapPosX + x*TILE_SIZE_X, mapPosY + y*TILE_SIZE_Y, TILE_SIZE_X, TILE_SIZE_Y, null);
                            g.fillRect(mapPosX + x*TILE_SIZE_X,mapPosY + y*TILE_SIZE_Y, TILE_SIZE_X, TILE_SIZE_Y);
                            //System.out.println("map drawing" + lightMap[(int)((x+70)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y]);
                        }
                }
       }
       public void scrollMapY (int scrAmount, int y, int x, int cy){
               mapPosY += scrAmount;
               mapPosY += scrAmount;
       }
       
       public void lightMap (int plrY, int plrX){
           int baseDarkness;
           
           //TORCH LIGHT STUFF <to be placed in own file?>
           for (int x = 0; x < WIDTH; x++){
               for (int y = 0; y < HEIGHT; y++){
                   if(data[x][y] == lmi.torch){
                       baseDarkness = lightMap [x+10][y].getAlpha();
                       int dispersalRate = baseDarkness/10;
                       for(int xl = 0; xl < 7; xl++){
                           for(int yl = 0; yl < 8; yl++){
                               if(y+yl < HEIGHT){
                                   if(y+yl > -1){
                                        
                                        lightMap [x][y] = new Color (0,0,0,10);
                                        lightMap [x+1][y] = new Color (0,0,0,15); 
                                        lightMap [x-1][y] = new Color (0,0,0,15); 
                                        lightMap [x][y+1] = new Color (0,0,0,15); 
                                        lightMap [x][y-1] = new Color (0,0,0,15);
                                        if(((dispersalRate*xl)+(dispersalRate*yl)) < 200){
                                            lightMap [x+xl][y+yl] = new Color (0,0,0,((dispersalRate*xl)+(dispersalRate*yl)));
                                            lightMap [x-xl][y+yl] = new Color (0,0,0,((dispersalRate*xl)+(dispersalRate*yl)));
                                        }
                                        else{
                                            lightMap [x+xl][y+yl] = new Color (0,0,0,200);
                                            lightMap [x-xl][y+yl] = new Color (0,0,0,200);  
                                        }
                                    }
                               }
                                    if(y-yl < HEIGHT){
                                        if(y-yl > -1){
                                            if(((dispersalRate*xl)+(dispersalRate*yl)) <= 200){
                                                lightMap [x-xl][y-yl] = new Color (0,0,0,((dispersalRate*xl)+(dispersalRate*yl)));
                                                lightMap [x+xl][y-yl] = new Color (0,0,0,((dispersalRate*xl)+(dispersalRate*yl))); 
                                            }
                                            else{
                                               lightMap [x-xl][y-yl] = new Color (0,0,0,200);
                                               lightMap [x+xl][y-yl] = new Color (0,0,0,200); 
                                            }
                                        }
                                    }
                           }
                        }
                    }
                }         
            }
       }
        
        
       public boolean blocked(int x,int y,int dir){ 
        try{
            //if(y <= cy){
            //------------------------------------------------------------------------------------->
            if(dir == 0){
            
            if(data[(int)((x+70)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground1){
                return true;  
            }
            if(data[(int)((x+70)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground2){
                return true;  
            }
            if(data[(int)((x+70)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground3){
                return true;  
            }
           
           
           if(data[(int)((x+150)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground1){
             return true;  
           }
           if(data[(int)((x+150)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground2){
             return true;  
           }
           if(data[(int)((x+150)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground3){
             return true;  
           }
           return false;
          }
          
            
          else if(dir == 1){
              
            if(data[(int)((x+20)-mapPosX)/TILE_SIZE_X][(int)((y+150) -mapPosY)/TILE_SIZE_Y] == lmi.ground1){
                return true;  
            }
            if(data[(int)((x+20)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground2){
                return true;  
            }
            if(data[(int)((x+20)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground3){
                return true;  
            }
           
           
           if(data[(int)((x+100)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground1){
             return true;  
           }
           if(data[(int)((x+100)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground2){
             return true;  
           }
           if(data[(int)((x+100)-mapPosX)/TILE_SIZE_X][(int)((y+150)-mapPosY)/TILE_SIZE_Y] == lmi.ground3){
             return true;  
           }
           return false;
          } 
            
        }
       catch(Exception es){
       }
        return false;
      }
      
}

If anyone can help me get shadows working so that blocks can produce shadows, or help me implement a better lighting system it would be much appreciated, It’s my first real game so I’m not quite sure how to go about it…

I made something like that a few weeks ago. I’ll try to explain what I made:

-First af all, the Tile class should have a variable lightLevel or something like that, set it to 0. That way that tile won’t be visible.(You can also make that if the light level is 0 don’t draw that tile for better performance :slight_smile: );

-On the draw method of the tile I used some black images with different alpha values . So first draw the tile and the alpha images just above. Make a switch or if’s and select the image according to the lightLevel.

-Draw a map using an array of Tiles.

-On the torch class I made that:


public Torch(int x, int y){

//change the light livel of all the adjacent tiles(make the shape that you want)
Map.tiles[x][y].lightLevel=0; 
Map.tiles[x+1][y].lightLevel=1;
Map.tiles[x+2][y].lightLevel=1; 
Map.tiles[x+3][y].lightLevel=2; 
Map.tiles[x][y+1].lightLevel=1;
Map.tiles[x][y+2].lightLevel=1;

//...and many more
}

So, when you place a torch on the map it will assign the lightLevel.

I hope it’s understable. Man, I suggest you to use more classes, it will simplify your work so much.

I’ve just realized that I write a semicolon after the parentheses xDD

Ah, fair enough, it’s slightly similar in that you’re using alpha-ed black images and I’m using alpha-ed black squares, I was planning on moving the torches stuff in to it’s own class, but you’re right I should give the tiles their own class, that’ll simplify things later. Also, did you have any shadows in yours or did you not bother?

Java is OOP so…make your game OOP as well ;D No I didn’t make shadows, but now I’m thinking how can I make them haha Post your progress and let us know how it’s going! Oh and wellcome to JGO :slight_smile:

Haha, fair enough, thanks! I’ll be sure to post up progress and let you know if I find out any ways of doing shadows…

I pondered on this for a while but eventually switched to OpenGL and use Pitbuller’s lighting today. Follow my signature for some info.
Or check out http://code.google.com/p/straightedge/ for Java2D.

That could be quite useful, I’ll take a look! -Thanks :slight_smile:

Wouldn’t using that library mean changing how I’m doing the mapping?

Few things before I get to your main question:

  1. As far as I know, it is not common to store game maps as 2 dimensional arrays of bufferedImages. This makes any sort of logic look quite messy and usually people tend towards having 2d arrays of ints if the tiles aren’t dynamic (or enumerations, if you like to keep it easy to read) (if they are dynamic, give them their own classes), which represent tiles, and are later converted by the renderer to render a static bufferedImage which represents that tile number. Using ints allows you to say:

if(tileNumber>=0 && tilenumber<=20){ //where only floor tiles have IDs between 0 and 20
//do stuff specific to all floor tiles
}

rather than

if(tile.equalTo(lmi.floor) || tile.equalto(lmi.floor2) || tile.equalto(lmi.floor3) etc…

Maybe it’s just me, but i think that although an array of buffered images may seem easier for rendering, it is not as neat when you use it in logic.

/////////////////////////////////

2.Rather than searching through every tile to find a torch, which can get quite intensive for larger maps, you may find it would be better to keep an arrayList of points where torches reside.

/////////////////////////////////

Now onto your main question. Depending on how you want your lighting to look, and how many torches you are going to be using, there are many different lighting systems you could use.

Probably one of the simplest to implement would be to check whether each tile is visible from the light source.

so you would need to make a method for checking the visibility from the torch tile to the destination tile, and use the result from that to figure out whether the tile should be lit or not.

For example:


public static boolean isTileAVisibleFromB(Point a, Point b){	//couldn't come up with a decent method name :P

	if(a.getX() == b.getX() && a.getY() == b.getY()) return true;

	if(a.getX() != b.getX()){			//To prevent dividing by zero
		double gradient = ((double)a.getY() - (double)b.getY()))/((double)a.getX() - (double)b.getX()));
		
		if(a.getX() > b.getX()){
			int length = a.getX() - b.getX();

			int x;
			int y;

			while(a.getX() + x != b.getX() && a.getY() + y != b.getY()){
				y = (int)(x*gradient);
				
				if( the tile[x + a.getX()][y + a.getY()] cannot let light through){	//Pseudocode, you will need to check whether this tile can let light through
					return false;
				}
				x++;
			}


		else{
			int length = b.getX() - a.getX();

			int x;
			int y;

			while(a.getX() + x != b.getX() && a.getY() + y != b.getY()){
				y = (int)(x*gradient);
				
				if(the tile[x + b.getX()][y + b.getY()] cannot let light through){	//Pseudocode, you will need to check whether this tile can let light through
					return false;
				}
				x++;
			}
	}
	else{
		int length = a.getY() - b.getY();

		int y;

		if(length>0){
			while(a.getY() + y != b.getY()){
				if( the tile[x + a.getX()][y + a.getY()] cannot let light through){	//Pseudocode, you will need to check whether this tile can let light through
					return false;
				}
				y++
			}
		}
		else{
						while(a.getY() + y != b.getY()){
				if( the tile[x + a.getX()][y + a.getY()] cannot let light through){	//Pseudocode, you will need to check whether this tile can let light through
					return false;
				}
				y++
			}
		}
	}

	return true;
}

That might not work completely, i just quickly typed it up in notepad, and there are some sections you need to fill in for yourself where I have just used pseudocode. You just need to check this before you light a tile, each time you were thinking of lighting a tile, and it will create sharp shadows, which are not completely realistic, but some people like 'em. If you wanted them with blurry edges there are a few different paths you could take, but just experiment! that code there should get the hard bit out of the way, but once you understand what I’ve written, you could customise it to include translucency, mirrors, coloured lighting, all sorts of stuff!.

One more thing, i’m not sure what you’re doing with the lighting at the moment, but there is no need to redo the lighting every frame, only whenever you change something in the area of the torch.

Good luck, post us the results :wink:

-Ciaran54

Wow, thats really helpful thanks!, with the everything posted here I should be able to take a good swing at getting this working. But I’m off to bed as I’m tired…
I’ll make sure to post up progress later this week. :slight_smile:

Sorry, I slept on that code and realised that it would not work for line gradients of greater than 1, because it would miss out bits of the line checking. A method I have used in the past is to, if the gradient is greater than 1, do 1/gradient and step by y++ rather than x++.

But this makes a huge mess of code, surely someone can think of a better way?

Determing visibility between a light and a tile is pretty easy, just raycast between them and check if there´s something in between. The problem here is performance. Doubling the radius of a light quadrouples the area the light covers, and therefore the number of raycasts. However, the raycasts also get a LOT more expensive since the extra raycasts needed are twice as long. In the end this scales VERY badly with larger lights, but if you only have a handful of lights with a radius of around 10 tiles or so you should be able to get realtime frame rates pretty easily.

I encountered the exact same problem when I was doing fog of war with “shadows” from walls. I ended up uploading a “wall map” to a texture and then doing the raycasts in a shader on the GPU for each tile. I got antialiased shadows thanks to free texture interpolation of the wall map, very simple code and almost no CPU cost at all since it´s handled by the GPU. Performance of lights with a radius of 10 tiles: around 50 000 lights at 60 FPS on relatively low end computers. Increasing the light size still has a very high cost though…

Are there any good raycasting tutorials?

Raycasting is the fancy name for what I described, all you need is an algorithm to draw a line. You could finish the one I started, make your own, or google the ‘Bresenham algorithm’ for more information on how to calculate lines efficiently

Take into mind what theagentd said, about the lights scaling badly. But be sure you only update the lights every frame if you absolutely need to. If the lighting isn’t going to change, You only need to calculate lighting when the level starts.If you can place your own blocks or lights, you only need to update that affects that area when you place a block/torch.

Fair enough, would the bit earlier about " if the gradient is greater than 1, do 1/gradient and step by y++ rather than x++." - would that work?
or are there better ways to do it?

EDIT: Also, why is it that your code doesn’t work above gradients of 1?

My code won’t work correctly for gradients above 1 because if you have a gradient below one, you need to increment the x value several times to see a change in the integer value of y. For a gradient greater than one, for every x increment, there have been several changes in the y value. So you will skip some values using my code, as it only ever implements x stepping.

I accidentally just forgot to write the second half of the code… :’(

By doing 1/gradient, and incrementing by y, you are essentially flipping the standard straight line equation from y=mx to x=y/m (or x=[1/m]y)by dividing both sides by m. you would need to change a few of the calculations, for it to work properly for y stepping, but it would definitely work.

The Bresenham algorithm is supposedly one of the most efficient ways of doing it, because it keeps clear of using double values and uses ints instead. I haven’t looked into how the Bresenham algorithm works, but if I were a beginner, I would use whatever made sense to me the most, because copying and pasting code when you don’t understand it and couldn’t rewrite it yourself is usually a bad habit to get into.

Yeah, as much as I want things to work… I also want to learn HOW they work… so I know it’s worth something :slight_smile:

Well, if you want the basic premise for my code, so you can modify it to work for you/rewrite it, this is the basic idea i used:

  1. Calculate the gradient and set one of the ends of the ray to be the graph origin.
    2a. Stepping through the x coordinates, use the equation y=mx (where m is the gradient) to find the corresponding y value.
    2b. take the x value and the calculated y value and check to see whether that tile is blocked
    2c. If the tile is blocked, it is not possible to light the final tile
    2c. If you have reached the final tile, it is possible for the light to travel this path
  2. Repeat steps 2 until all tiles in the possible area have been checked.

I hope this has helped you, good luck :wink:

That sounds simple, but it looks a bit more of a pain in the arse in practice… :stuck_out_tongue: Anyway, thanks for all the help, after I’ve changed my map system from an array of buffered images… I’ll have a go at it, then post my results