6
\$\begingroup\$

I was inspired by this to write a graphic snow animation in Java/Processing. It works great, but it has some design issues.

The full project is here on github;

Main

package com.jatkin.snowday;

import processing.core.PApplet;
import processing.core.PGraphics;

/**
 * Created by Jarrett on 02/04/16.
 */
public class Main extends PApplet {

    public static void main(String[] args) {PApplet.main(Main.class.getCanonicalName());}

    final int backgroundColor = color(0, 146, 178);
    final World world = new World();

    /**
     * Processing 3 has changed the basic format for starting an app from raw
     * java. Size now must be called from `settings`.
     */
    @Override
    public void settings() {
        size(500, 300, FX2D);
    }

    @Override
    public void draw() {
        fill(255);
        noStroke();

        background(backgroundColor);
        world.tick(sketchWidth(), sketchHeight());
        world.render(getGraphics());
    }
}

World

package com.jatkin.snowday;

import com.jatkin.snowday.snowflake.FlakeFactory;
import com.jatkin.snowday.snowflake.Snowflake;
import processing.core.PGraphics;
import processing.core.PVector;

import java.util.ArrayList;
import java.util.List;

/**
 * This class controls all physics for SnowDay.
 *
 * Created by Jarrett on 02/04/16.
 */
public class World {
    private float gravity;
    private List<Snowflake> flurry;
    private Wind wind;
    private int flakeCountTarget;


    public World() { this(2); } // FIXME magic number

    public World(int gravity) {
        this.gravity = gravity;
        flurry = new ArrayList<>();
        wind = new Wind();
        flakeCountTarget = 200;// FIXME magic number
    }

    /**
     * Advance the snow animation 1 frame
     */
    public void tick(int screenWidth, int screenHeight) {
        wind.tick();
        manageSnowflakeCount(screenWidth, screenHeight);
        edgeWrapSnowflakes(screenWidth, screenHeight);
        updateSnowflakePosition();
    }

    /**
     * Wrap flakes around edged of the screen.
     */
    private void edgeWrapSnowflakes(int screenWidth, int screenHeight) {
        flurry.forEach(snowflake -> {
            if (snowflake.getPosition().x > screenWidth)
                snowflake.getPosition().x = 0;
            else if (snowflake.getPosition().x < 0)
                snowflake.getPosition().x = screenWidth;
        });
    }

    /**
     * Reset off screen flakes.
     * @param screenWidth
     * @param screenHeight
     */
    private void manageSnowflakeCount(int screenWidth, int screenHeight) {
        flurry.removeIf(flake -> flake.getPosition().y > screenHeight);

        int newFlakeCount = (int)Math.sqrt(flakeCountTarget - flurry.size());
        for (int i = 0; i < newFlakeCount; i++)
            flurry.add(FlakeFactory.genRandomSnowflake(screenWidth));

    }

    /**
     * Apply wind, gravity and rotation to all the snowflakes
     */
    private void updateSnowflakePosition() {
        for (Snowflake flake : flurry) {
            PVector flakePosition = flake.getPosition();
            float weight = flake.getWeight();

            float downwardsMotion = weight*gravity;
            float windMotion = (wind.getSpeed() / weight);

            flake.getSpeed().set(windMotion, downwardsMotion);
            flakePosition.add(flake.getSpeed());

            flake.setRotation(flake.getRotation() + flake.getRotationSpeed());
        }
    }

    /**
     * Render all the snowflakes this world has onto the surface
     * @param surface
     */
    public void render(PGraphics surface) {
        for (Snowflake flake : flurry) {
            flake.render(surface);
        }
    }


    public float getGravity() {
        return gravity;
    }
}

Wind

package com.jatkin.snowday;

import static java.lang.Math.*;

/**
 * Created by Jarrett on 02/05/16.
 */
public class Wind {
    private final static float maxWind = 6;
    private final static int maxDurationInMs = 5000;
    private final static int minDurationInMs = 2000;
    private float targetSpeed = 0;
    private float speed = 0;
    private long windStopTime = 0;

    public void tick() {
        long now = System.currentTimeMillis();
        if (now > windStopTime)
            calculateNewWind();
        else
            interpolateWindSpeed();
    }

    private void calculateNewWind() {
        float newWindSpeed = (float)sqrt(random()*maxWind*maxWind);
        if (random()>0.5) newWindSpeed = -newWindSpeed;
        int newDuration = (int) (random() * (maxDurationInMs - minDurationInMs)) + minDurationInMs;

        targetSpeed = newWindSpeed;
        windStopTime = newDuration + System.currentTimeMillis();
    }

    private void interpolateWindSpeed() {
        float speedDiff = targetSpeed - speed;
        float speedChange = (float) (sqrt(abs(speedDiff))/15);

        if (speedDiff > 0)
            speed += speedChange;
        else
            speed -= speedChange;
    }

    public float getSpeed() {
        return speed;
    }
}

snowflake.Snowflake

package com.jatkin.snowday.snowflake;

import processing.core.PGraphics;
import processing.core.PVector;

/**
 * Created by Jarrett on 02/04/16.
 */
public abstract class Snowflake {

    private float weight = 1;
    private float rotation = 0;
    private float rotationSpeed = 0;
    final private PVector position = new PVector();
    final private PVector speed = new PVector();

    /**
     * Render this flake on the surface
     * @param surface
     */
    abstract public void render(PGraphics surface);


    public float getRotationSpeed() {
        return rotationSpeed;
    }

    public void setRotationSpeed(float rotationSpeed) {
        this.rotationSpeed = rotationSpeed;
    }

    public PVector getPosition() {
        return position;
    }

    public float getWeight() {
        return weight;
    }

    public void setWeight(float weight) {
        this.weight = weight;
    }

    public PVector getSpeed() {
        return speed;
    }

    public float getRotation() {
        return rotation;
    }

    public void setRotation(float rotation) {
        this.rotation = rotation;
    }
}

snowflake.SquareSnowflake

package com.jatkin.snowday.snowflake;

import processing.core.PConstants;
import processing.core.PGraphics;

/**
 * Created by Jarrett on 02/04/16.
 */
public class SquareSnowflake extends Snowflake {
    private final float size = 13;

    @Override
    public void render(PGraphics surface) {
        surface.pushMatrix();

        surface.translate(getPosition().x, getPosition().y - size/2);
        surface.rotate(getRotation());
        surface.rectMode(PConstants.CENTER);
        surface.rect(0, 0, size, size);

        surface.popMatrix();
    }

}

snowflake.FlakeFactory

package com.jatkin.snowday.snowflake;

/**
 * Created by Jarrett on 02/05/16.
 */
public class FlakeFactory {

    public static Snowflake genRandomSnowflake(int maxXLoc) {
        Snowflake flake = new SquareSnowflake();
        flake.setWeight((float)(Math.random()/2)+1);      // FIXME: magic numbers
        flake.setRotationSpeed((float)(Math.random()*Math.PI/18d));
        flake.getPosition().x = (int)(Math.random()*maxXLoc);
        return flake;
    }

}

The questions I have are ordered in greatest to least importance to me.

My main issue with this code is the magic numbers. Magic numbers are everywhere. I was able to sensibly move some of the numbers to private static final fields, but some of the rest don't seem to belong anywhere. For example FlakeFactory#genRandomSnowflake:

public static Snowflake genRandomSnowflake(int maxXLoc) {
    Snowflake flake = new SquareSnowflake();
    flake.setWeight((float)(Math.random() / 2 ) + 1);
    flake.setRotationSpeed((float)(Math.random() * Math.PI / 18d));
    flake.getPosition().x = (int)(Math.random() * maxXLoc);
    return flake;
}

Many magic numbers exist here, mostly to keep the animation rate sane.

Other than that, how does my overall design look? I encapsulated nearly all the physics code in World. Is it a good or bad idea?

I think most of the code is reasonably split up into useful, descriptive methods with some documentation where I thought it was required. How clean is the overall code inside the methods? Documentation quality?

Keep in mind that I am completely self taught.


Installing processing

Processing isn't on Maven central, so to install it you need to download it.

  1. Go to https://processing.org/download/ and download version 3.0.1
  2. Unpack the download and locate the \core\library folder
  3. Add the entire folder to your classpath
\$\endgroup\$
  • \$\begingroup\$ You're making square snowflakes too? \$\endgroup\$ – 200_success Feb 5 '16 at 20:08
  • \$\begingroup\$ I'm not very artsy. \$\endgroup\$ – J Atkin Feb 5 '16 at 20:09
2
\$\begingroup\$

That looks really good! I just have a few improvements, nothing big.

Wind.interplorateWindSpeed()

A math function called signum could be used instead of if statements.

float speedDiff = targetSpeed - speed;

float speedChange = (float) (sqrt(abs(speedDiff))/15);

speed += speedChange * signum(speedDiff);

Wind

You might consider moving all the private floats values into a constructor, just for consistency.

Thoughts

Really these are just small tweaks, you have some very well-written and documented code!

The next thing to do would be to make more types of snowflakes (hexagons, stars, textures) for variety.

\$\endgroup\$
  • \$\begingroup\$ sqrt() takes care of abs()? \$\endgroup\$ – 200_success Feb 6 '16 at 17:08
  • \$\begingroup\$ @200_success For sure not, unless you want NaN. \$\endgroup\$ – maaartinus Feb 6 '16 at 17:28
  • \$\begingroup\$ I was using just sqrt(), but all the snowflakes disappeared when the wind changed. \$\endgroup\$ – J Atkin Feb 6 '16 at 19:00
  • \$\begingroup\$ @maaartinus sorry, my bad. \$\endgroup\$ – Blue Feb 6 '16 at 19:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.