3
\$\begingroup\$

I'm creating a top-down game for my A-level/college computer science project, and I'm trying to implement rain. The game needs to be usable on any platform, so the use of external libraries is out of the question. I currently have the GUI running off Java's Graphics package, and the game is rendered pixel-per-pixel from an array of pixels. The aim would be to have a rain effect that looks something like this http://d2436y6oj07al2.cloudfront.net/spff/previews/vbme0577.jpg. If this is possible, this would be really good. Currently I also have a particle class and particle spawners', which add the blood effect and effect of projectiles "exploding" when they hit a wall.

Is there a way to implement some rain from what I already have? I attempted to implement a Rain class, which spawns particles in a random place within bounds on an area of the screen, to make it look like it was coming from the perspective of the blocks.

Here is the rain i've tried to add (blue particles): enter image description here

The issue is it moves and looks nothing like the image linked above. Also, here is my Rain class, which is added currently by the player just for testing, but the end game will be to have a "WeatherManager" class that handles all the weather changes and weather spawning (rain, snow etc).

package com.ritcat14.GotYourSix.entity.particle;

import java.util.Random;

import com.ritcat14.GotYourSix.Game;
import com.ritcat14.GotYourSix.entity.Entity;
import com.ritcat14.GotYourSix.graphics.Sprite;
import com.ritcat14.GotYourSix.level.Level;

public class Rain extends Entity {

    private int     time      = 0;    //time it is raining for
    private int     currTime  = 0;
    private boolean timeOfDay = false; //if it is night or day (false - night, true - day)
    private Level   level     = null;
    private Sprite  drop      = null;
    private int     amount    = 0;

    public Rain(int time, boolean timeOfDay, Level level) {
        init(level);
        this.time = time;
        this.timeOfDay = timeOfDay;
        this.level = level;
        if (timeOfDay) {
            drop = new Sprite(1, 0x87F7FF);
        } else {
            drop = new Sprite(1, 0xEFFBFF);
        }
    }

    public void update() {
        currTime++;
        if (currTime > time) {
            remove();
        } else {
            Random ran = new Random();
            if (time % (ran.nextInt(120) + 1) == 0) {
                int x = ran.nextInt(Game.getWindowWidth());
                int y = ran.nextInt(Game.getWindowHeight() / 4) + 100;
                level.add(new Particle(x, y, 1000, drop));
                amount++;
            }
        }
    }

    public void setDay(boolean dayTime) {
        this.timeOfDay = dayTime;
    }

}

Here is my particle class:

package com.ritcat14.GotYourSix.entity.particle;

import java.util.Random;

import com.ritcat14.GotYourSix.entity.Entity;
import com.ritcat14.GotYourSix.entity.mob.Player;
import com.ritcat14.GotYourSix.entity.projectile.Projectile;
import com.ritcat14.GotYourSix.graphics.Screen;
import com.ritcat14.GotYourSix.graphics.Sprite;

public class Particle extends Entity {

    private Sprite   sprite = null;

    private int      life = 0;
    private int      time = 0;

    protected double xx = 0, yy = 0, zz = 0;
    protected double xa = 0, ya = 0, za = 0;
    private Random   ran = new Random();

    public Particle(int x, int y, int life, Sprite sprite) {
        this.x = x;
        this.y = y;
        this.xx = x;
        this.yy = y;
        this.life = life + (random.nextInt(50) - 25);
        this.sprite = sprite;
        this.xa = random.nextGaussian();
        this.ya = random.nextGaussian();
        this.zz = random.nextFloat() + 2.0;
        int col = ran.nextInt(4);
        if (sprite != Sprite.particle_blood) {
            if (Projectile.weapon == Projectile.Weapon.FIREDCANNON || Projectile.weapon == Projectile.Weapon.FIREDARROW) {
                if (col == 0)
                   this.sprite = new Sprite(2, 0xff000000);
                if (col == 1)
                    this.sprite = new Sprite(2, 0xffF8A800);
                if (col == 2)
                    this.sprite = new Sprite(2, 0xffF8F8C8);
                if (col == 3)
                    this.sprite = new Sprite(2, 0xffF8A800);
            } else if (Projectile.weapon == Projectile.Weapon.FIREBALL || Projectile.weapon == Projectile.Weapon.FIREWALL) {
                if (col == 0)
                    this.sprite = new Sprite(2, 0xffF86800);
                if (col == 1)
                    this.sprite = new Sprite(2, 0xffF8A800);
                if (col == 2)
                    this.sprite = new Sprite(2, 0xffF8F8C8);
                if (col == 3)
                    this.sprite = new Sprite(2, 0xffF8D840);
            } else if (Projectile.weapon == Projectile.Weapon.ICEDCANNON || Projectile.weapon == Projectile.Weapon.ICEDARROW) {
                if (col == 0)
                    this.sprite = new Sprite(2, 0xff427AFF);
                if (col == 1)
                    this.sprite = new Sprite(2, 0xff607BFF);
                if (col == 2)
                    this.sprite = new Sprite(2, 0xff7C9BFF);
                if (col == 3)
                    this.sprite = new Sprite(2, 0xff000000);
            } else if (Projectile.weapon == Projectile.Weapon.ICEBALL || Projectile.weapon == Projectile.Weapon.ICEWALL) {
                if (col == 0)
                    this.sprite = new Sprite(2, 0xff427AFF);
                if (col == 1)
                    this.sprite = new Sprite(2, 0xff607BFF);
                if (col == 2)
                    this.sprite = new Sprite(2, 0xff7C9BFF);
                if (col == 3)
                    this.sprite = new Sprite(2, 0xffD3DEFF);
            }
        }
    }

    public void update() {
        time++;
        if (time >= Integer.MAX_VALUE - 100)
        time = 0; //Safety check
        if (time > life)
            remove();
        za -= 0.1;

        if (zz < 0) {
            zz = 0;
            za *= -0.55;
            xa *= 0.4;
            ya *= 0.4;
        }

        move(xx + xa, (yy + ya) + (zz + za));
    }

    private void move(double x, double y) {
        if (collision(x, y)) {
            this.xa *= -0.5;
            this.ya *= -0.5;
            this.za *= -0.5;
        }
        this.xx += xa;
        this.yy += ya;
        this.zz += za;
    }

    public boolean collision(double x, double y) {
        boolean solid = false;
        for (int c = 0; c < 4; c++) {
            double xt = (x - c % 2 * 16) / 16;
            double yt = (y - c / 2 * 16) / 16;
            int ix = (int)Math.ceil(xt);
            int iy = (int)Math.ceil(yt);
            if (c % 2 == 0)
                ix = (int)Math.floor(xt);
            if (c / 2 == 0)
                iy = (int)Math.floor(yt);
            if (level.getTile(ix, iy).solid())
                solid = true;
        }
        return solid;
    }

    public void render(Screen screen) {
        screen.renderSprite((int)xx - 1, (int)yy - (int)zz - 1, sprite, true);
    }

}

Any ideas as to how to improve and/or make it appear to "fall" to the ground? I managed this with standard particles, but I can't get my head round how "Rain" would appear to "fall" in a 2D world. Any ideas? Thanks

\$\endgroup\$
4
  • \$\begingroup\$ You need to do some research, give it a try, and show us where you get stuck. Otherwise the question is too broad / opinion-based and it will be closed. For now I won't put a close vote on it - to give you time to fix it up. \$\endgroup\$
    – Engineer
    Nov 4, 2015 at 15:55
  • \$\begingroup\$ @ArcaneEngineer edited as suggested \$\endgroup\$
    – Kris Rice
    Nov 4, 2015 at 17:44
  • \$\begingroup\$ Good, you should get some answers soon, hopefully. \$\endgroup\$
    – Engineer
    Nov 4, 2015 at 18:05
  • 1
    \$\begingroup\$ Here's an example of one implementation of the basic effect you're looking for. It's written in processing which is a Java based framework, but it could provide a starting place. If it helps, great! if not, well hopefully you enjoyed watching it (I did) youtube.com/watch?v=17WoOqgXsRM&ab_channel=TheCodingTrain \$\endgroup\$ Oct 23, 2017 at 15:12

2 Answers 2

1
\$\begingroup\$

I would say first, your problem is more one of deciding on an artistic style, and not as much about code. Like with a lot of things artistic, there is no right answers, but there are definitely ideas and approaches.

Second, your game is not 2D - it's 2.5D in a pseudo 3D way, just like isometric is 2D, but also 3D'ish. Your characters are not drawn from a top down view for example, which hugely changes the artistic approach.

Focusing on Artistic Style

You could just do animations of rain drop splashes on the ground, but that could look wrong. The player sees the drop hit the ground, but they don't see the drop fall. So based on that, I think you have to animate rain falling before you have it hit the ground.

You could have all drops "fall" from the top of the screen, but it might not quite look right. It does not match the pseudo 3D/isometric stye.

Solutions

Personally what I would do is manage this with two sprites for each raindrop: one sprite for a falling drop, and another sprite for a drop splash on the ground.

Falling rain drop sprites would be spawned randomly across the map at X/Y locations, and be assigned a starting altitude (pseudo Z coordinate). Then the sprite is animated with the following conditions...

  • Sprite initial spawn coordinates should be the entire space of your game screen, plus additional space so that some drops can spawn off the top of the screen and eventually fall into view from the top
  • Falling sprites are drawn with the Y axis of the screen treated as a pseudo Z axis
  • The altitude controls how far down the screen the drop will fall before it hits the ground
  • A drop sprite starts off invisible, and fades in fairly quickly as it falls so that they don't simply appear suddenly
  • When a drop sprite reaches zero altitude, it is remove and replaced with a "splash" sprite

Splash sprites work as follows... * It is a simple, non looping animated sprite of a rain drop splash * They are spawned when a drop reaches zero altitude * If a drop falls off the screen, then no splash sprite is drawn

Stylistic Control and Improvements

This approach will give you a lot of control over the rain style both visually and programmatically, and you've got some interesting space here to do things.

If artistically it's not right, work on your sprites.

If you want heavier rain, increase the random chance of a drop being spawned, or decrease it if you want lighter rain.

Have raindrops hit objects and not the ground. So rain hits the branches of a tree (with no splash) and not the ground under. Or give the player an umbrella.

Acoustically, you can also do some great audio effects based on rain intensity. Technically you could trigger a little "splash" sound on each drop falling, so the sound of the rain fully matches what is drawn visually. This might be a problem with a lot of raindrops, but it definitely would work for light rain, or when a rainstorm is just starting.

Have clouds (sprites) drift across the screen, and have drops spawn only in clouds.

\$\endgroup\$
2
\$\begingroup\$

Try creating a 3-4 frame animation of a semi-transparent blue-white dot that moves downward a few pixels before expanding out to a 1-2 pixel radius circle. Then just create random instances of this animation all over the map. Should give the appearance of rain droplets. From there you can adjust it by maybe adding more frames or adjusting the shape when it hits the ground. Try adjusting the transparency of the rain droplet throughout the animation so it's more opaque at the start.

\$\endgroup\$
2
  • \$\begingroup\$ I'd rather make it transparent at the start, to avoid droplets suddenly appearing from nowhere \$\endgroup\$ Aug 8, 2019 at 13:01
  • \$\begingroup\$ @trollingchar Sure, that could work. \$\endgroup\$ Aug 8, 2019 at 13:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .