# bigf00t的博客

## Generating random landscapes for a computer game

(2011-11-27 22:39:13)

### programming

Because I don't know of any useful project but like to try out some programming things, I decided to do a small game. It should be not too difficult because I'll do it in my free time and it should be small enough to finish within some months. The idea is something Worms-like with tanks, much like the good old "scorched earth" game.

One thing I wondered was, how to generate a landscape with rounded hills. This blog post will explain the solution I have decided to do. First of all, I learned that "Perlin Noise" can generate random numbers that look, if plotted, much like the result of the gradient tool of photoshop (meaning that they have are smooth slopes). If I plot them in Excel, they give me exactly what I want: A hilly landscape. Mr. Perlin has put a reference implementation online which I used for my project: http://cs.nyu.edu/~perlin/noise/. Now that I could generate the shape of the hills, how to fill them with colour or texture?

For this, I had three ideas:
1. Generate all pixel by pixel, maybe some random noise to make it look nice.
--> This will be very hard. It has no doubt the greatest flexibility but I don't want to spend hours just doing a background image.
2. Take a picture and cut away the top until the line of the perlin noise.
--> This would be easy. But it would take away the surface. I had in mind to have everything look like dirt but the top like grass. With this solution, all the grass would be cut off.
3. Take a picture and shift the columns down by the amount of the perlin values.
--> This is also not too difficult, it leaves my grass intact and will also give a nice layer effect to the layers in my dirt.

So I decided to go with solution number 3.

To test it, I have written a small application that offers levers to manipulate four parameters to generate the landscape. The result will first be written into a CSV-File (not really separated by comma but by lines. It can be opened and plotted in Excel though) and saved as a PNG file.

The test program consists of three classes:

1. The reference implementation of Ken Perlin. (I only use one dimension of the noise)
2. MapGenerator - The class where everything happens
3. MapGeneratorMain - A window to change params and run the program

For the generation of levels, I need two resources of the same size:

1. A Landscape texture

2. A Sky texture
To generate the landscape, I first generate perlin noise in the length of the image's width. Then I take the generated noise, give it an offset and shift each pixel column of the landscape texture down by the amount given in the onedimensional Perlin Noise array. The result is a nice mountainous landscape. I draw the shifted landscape directly onto the sky texture because it looks better this way. In my real game, I'll just leave the rest transparent and draw them one by one.

After processing, the result looks like this:

All parameters can be controlled using a small Swing GUI which I built to play around a bit.

The code for my two classes follows below. For the Perlin Noise Class, go to Ken Perlin's Homepage and copy and paste the ImprovedNoise class.

MapGenerator.java

 ```package levelgen; import javax.imageio.ImageIO; import javax.swing.JFrame; import java.awt.AlphaComposite; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; public class MapGenerator extends JFrame {      private static final long serialVersionUID = -7214908856495990572L;   private ArrayList heights;   private BufferedImage levelFile = null;   private BufferedImage outputFile = null;      public MapGenerator() {     setTitle("Level Generator");     initialize();   }      private void initialize() {   }      public BufferedImage generate(float offset, float valleyDepth, float slope1, float slope2) {     try {       levelFile = ImageIO.read(new File("data/level2.png"));       // prefill the ouput with the background       outputFile = ImageIO.read(new File("data/himmel.png"));     } catch (IOException e) {       e.printStackTrace();     }     Graphics2D g2d = outputFile.createGraphics();     g2d.setComposite(AlphaComposite.Src);     // move the pixels from the original down to create mountains     pixelMoving(offset, valleyDepth, slope1, slope2);     g2d.dispose();     try {       // save image file       ImageIO.write(outputFile, "png", new FileOutputStream("data/output.png"));       // save a csv       saveHeightsToFile(heights);     } catch (FileNotFoundException e) {       e.printStackTrace();     } catch (IOException e) {       e.printStackTrace();     }     return outputFile;   }      public int getMinimum(ArrayList list) {     if (list == null || list.size() < 1)       throw new IllegalArgumentException();     int tmpmin = list.get(0);     for (int i : list) {       if (tmpmin < i)         tmpmin = i;     }     return tmpmin;   }      public void makeHeightsPositive(ArrayList list) {     int minimum = getMinimum(list);     if (minimum < 0) {       minimum *= -1;     }     for (int i = 0; i < list.size(); i++) {       list.set(i, list.get(i) + minimum);     }   }      private void saveHeightsToFile(ArrayList list) {     FileWriter fstream;     try {       fstream = new FileWriter("data/heights.csv");       BufferedWriter out = new BufferedWriter(fstream);       for (int height : heights) {         // the heights array contains the valleys, not the mountains.         // we have to subtract it from the height to get the real         // mountains.         out.write((levelFile.getHeight() - height) + "\n");       }       out.close();       fstream.close();     } catch (IOException e) {       e.printStackTrace();     }   }      public void pixelMoving(float offset, float valleyDepth, float slope1, float slope2) {     heights = new ArrayList();     for (int i = 0; i < levelFile.getWidth(); i++) {       int n = (int) (ImprovedNoise.noise(valleyDepth, valleyDepth, valleyDepth) * slope2);       // With each cycle, increment xoff       valleyDepth += slope1;       heights.add(n + (int) offset);     }     // lift the whole surface so that no negative numbers occur     makeHeightsPositive(heights);     // Loop through every pixel column     for (int x = 0; x < levelFile.getWidth(); x++) {       // Loop through every pixel row       for (int y = 0; y < levelFile.getHeight(); y++) {         if (levelFile.getHeight() > y + heights.get(x)) {           // Get a pixel           int rgb = levelFile.getRGB(x, y);           outputFile.setRGB(x, y + heights.get(x), rgb);         }       }     }   } }```

MapGeneratorMain.java
 ```package levelgen; import java.awt.EventQueue; import javax.swing.JFrame; import java.awt.BorderLayout; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JSlider; import java.awt.GridLayout; import javax.swing.SwingConstants; import javax.swing.JButton; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.awt.image.BufferedImage; public class MapGeneratorMain {   private JFrame frame;      public static void main(String[] args) {     EventQueue.invokeLater(new Runnable() {       public void run() {         try {           MapGeneratorMain window = new MapGeneratorMain();           window.frame.setVisible(true);         } catch (Exception e) {           e.printStackTrace();         }       }     });   }      public MapGeneratorMain() {     initialize();   }   private JSlider sliderVerticalOffset;   private JSlider sliderValleyDepth;   private JSlider sliderSlope1;   private JSlider sliderSlope2;      private void initialize() {     frame = new MapGenerator();     frame.setBounds(100, 100, 800, 600);     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);     frame.getContentPane().setLayout(new BorderLayout(0, 0));     JPanel panel_1 = new JPanel();     frame.getContentPane().add(panel_1, BorderLayout.CENTER);     panel_1.setLayout(new BorderLayout(0, 0));     JPanel panel = new JPanel();     panel_1.add(panel);     panel.setLayout(new GridLayout(0, 4, 0, 0));     sliderVerticalOffset = new JSlider();     panel.add(sliderVerticalOffset);     sliderVerticalOffset.setMaximum(200);     sliderVerticalOffset.setMajorTickSpacing(20);     sliderVerticalOffset.setPaintLabels(true);     sliderVerticalOffset.setMinorTickSpacing(5);     sliderVerticalOffset.setPaintTicks(true);     sliderVerticalOffset.setValue(100);     sliderVerticalOffset.setOrientation(SwingConstants.VERTICAL);     sliderValleyDepth = new JSlider();     sliderValleyDepth.setValue(100);     sliderValleyDepth.setMaximum(200);     panel.add(sliderValleyDepth);     sliderValleyDepth.setMajorTickSpacing(20);     sliderValleyDepth.setPaintLabels(true);     sliderValleyDepth.setPaintTicks(true);     sliderValleyDepth.setMinorTickSpacing(5);     sliderValleyDepth.setOrientation(SwingConstants.VERTICAL);     sliderSlope1 = new JSlider();     sliderSlope1.setMaximum(200);     panel.add(sliderSlope1);     sliderSlope1.setMajorTickSpacing(20);     sliderSlope1.setPaintLabels(true);     sliderSlope1.setPaintTicks(true);     sliderSlope1.setMinorTickSpacing(5);     sliderSlope1.setOrientation(SwingConstants.VERTICAL);     sliderSlope2 = new JSlider();     sliderSlope2.setMajorTickSpacing(20);     sliderSlope2.setMinorTickSpacing(5);     sliderSlope2.setValue(100);     sliderSlope2.setMaximum(200);     sliderSlope2.setPaintTicks(true);     sliderSlope2.setPaintLabels(true);     sliderSlope2.setOrientation(SwingConstants.VERTICAL);     panel.add(sliderSlope2);     JPanel panel_2 = new JPanel();     panel_1.add(panel_2, BorderLayout.SOUTH);     panel_2.setLayout(new GridLayout(0, 4, 0, 0));     JLabel lblNewLabel = new JLabel("Vertical Offset");     lblNewLabel.setHorizontalAlignment(SwingConstants.CENTER);     panel_2.add(lblNewLabel);     JLabel lblParam = new JLabel("Valley Depth ( x 1)");     panel_2.add(lblParam);     lblParam.setVerticalAlignment(SwingConstants.TOP);     lblParam.setHorizontalAlignment(SwingConstants.CENTER);     JLabel lblParam_1 = new JLabel("Slope 1 (x 0.0001)");     panel_2.add(lblParam_1);     lblParam_1.setVerticalAlignment(SwingConstants.TOP);     lblParam_1.setHorizontalAlignment(SwingConstants.CENTER);     JLabel lblParam_2 = new JLabel("Slope 2 (x 1)");     panel_2.add(lblParam_2);     lblParam_2.setVerticalAlignment(SwingConstants.TOP);     lblParam_2.setHorizontalAlignment(SwingConstants.CENTER);     JButton btnGenerate = new JButton("Generate");     btnGenerate.addActionListener(new ActionListener() {       public void actionPerformed(ActionEvent arg0) {         MapGenerator mapGenerator = new MapGenerator();         BufferedImage outputFile = mapGenerator.generate((float) sliderVerticalOffset.getValue(),             (float) sliderValleyDepth.getValue(), (float) sliderSlope1.getValue() * 0.0001f,             (float) sliderSlope2.getValue());         ImageIcon icon = new ImageIcon();         icon.setImage(outputFile);         JOptionPane.showMessageDialog(null, icon);       }     });     frame.getContentPane().add(btnGenerate, BorderLayout.SOUTH);   } }```

0

• 评论加载中，请稍候...

验证码： 请点击后输入验证码 收听验证码

发评论

以上网友发言只代表其个人观点，不代表新浪网的观点或立场。

新浪BLOG意见反馈留言板　不良信息反馈　电话：4006900000 提示音后按1键（按当地市话标准计费）　欢迎批评指正

新浪公司 版权所有