加载中…
正文 字体大小:

Generating random landscapes for a computer game

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

perlin-noise

java

level

generate

landscape

game

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.

Generating <wbr>random <wbr>landscapes <wbr>for <wbr>a <wbr>computer <wbr>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 textureGenerating <wbr>random <wbr>landscapes <wbr>for <wbr>a <wbr>computer <wbr>game

  2. A Sky textureGenerating <wbr>random <wbr>landscapes <wbr>for <wbr>a <wbr>computer <wbr>game
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:
Generating <wbr>random <wbr>landscapes <wbr>for <wbr>a <wbr>computer <wbr>game

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

Generating <wbr>random <wbr>landscapes <wbr>for <wbr>a <wbr>computer <wbr>game


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<Integer> 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 csv
      saveHeightsToFile(heights);
    catch (FileNotFoundException e{
      e.printStackTrace();
    catch (IOException e{
      e.printStackTrace();
    }
    return outputFile;
  }

  
  public int getMinimum(ArrayList<Integer> list{
    if (list == null || list.size() 1)
      throw new IllegalArgumentException();

    int tmpmin list.get(0);
    for (int list{
      if (tmpmin i)
        tmpmin i;
    }
    return tmpmin;
  }

  
  public void makeHeightsPositive(ArrayList<Integer> list{
    int minimum getMinimum(list);
    if (minimum 0{
      minimum *= -1;
    }

    for (int 0list.size()i++{
      list.set(i, list.get(iminimum);
    }
  }

  
  private void saveHeightsToFile(ArrayList<Integer> 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<Integer>();
    for (int 0levelFile.getWidth()i++{
      int (int(ImprovedNoise.noise(valleyDepth, valleyDepth, valleyDepthslope2);
      // With each cycle, increment xoff
      valleyDepth += slope1;
      heights.add((intoffset);
    }

    // lift the whole surface so that no negative numbers occur
    makeHeightsPositive(heights);

    // Loop through every pixel column
    for (int 0levelFile.getWidth()x++{
      // Loop through every pixel row
      for (int 0levelFile.getHeight()y++{
        if (levelFile.getHeight() heights.get(x)) {
          // Get pixel
          int rgb levelFile.getRGB(x, y);
          outputFile.setRGB(x, 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(100100800600);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    frame.getContentPane().setLayout(new BorderLayout(00));

    JPanel panel_1 new JPanel();
    frame.getContentPane().add(panel_1, BorderLayout.CENTER);
    panel_1.setLayout(new BorderLayout(00));

    JPanel panel new JPanel();
    panel_1.add(panel);
    panel.setLayout(new GridLayout(0400));

    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(0400));

    JLabel lblNewLabel new JLabel("Vertical Offset");
    lblNewLabel.setHorizontalAlignment(SwingConstants.CENTER);
    panel_2.add(lblNewLabel);

    JLabel lblParam new JLabel("Valley Depth 1)");
    panel_2.add(lblParam);
    lblParam.setVerticalAlignment(SwingConstants.TOP);
    lblParam.setHorizontalAlignment(SwingConstants.CENTER);

    JLabel lblParam_1 new JLabel("Slope (x 0.0001)");
    panel_2.add(lblParam_1);
    lblParam_1.setVerticalAlignment(SwingConstants.TOP);
    lblParam_1.setHorizontalAlignment(SwingConstants.CENTER);

    JLabel lblParam_2 new JLabel("Slope (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((floatsliderVerticalOffset.getValue(),
            (floatsliderValleyDepth.getValue()(floatsliderSlope1.getValue() 0.0001f,
            (floatsliderSlope2.getValue());
        ImageIcon icon new ImageIcon();
        icon.setImage(outputFile);
        JOptionPane.showMessageDialog(null, icon);
      }
    });
    frame.getContentPane().add(btnGenerate, BorderLayout.SOUTH);
  }

}

阅读 评论 收藏 转载 喜欢 打印举报
已投稿到:
  • 评论加载中,请稍候...
发评论

       

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

    发评论

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

      

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

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有