Animated grass using LibGDX

A little and easy tutorial today, but which can add a nice value to your games: we’re going to animate some grass to enhance the floors. Indeed, even if you really suck at graphics as I do, you can fake the user into thining it’s still awesome by animating everything! If it doesn’t look pretty, make it move!

Of course, I don’t claim that’s the best way to implement animated grass, but I found the effect quite cool and easy to achieve, so why not share it? :)

1. Initialization

We first need to draw some static grass on the screen, that’s our starting point. Two choices: (1) use one sprite per blade, or (2) combine multiple blades in a long sprite. Since grass blades only look nice when they overlap a lot with each other, solution 1 would be inefficient and would quickly lead to very bad performances in big scenes. Therefore, I randomly combined four drawings of blades in a single sprite, as follows:

Now, we need to initialize our game and draw this sprite on the screen. The code I used was this one:

publicclass AnimatedGrassApp extendsApplicationAdapter {
    privateOrthographicCamera camera;
    privateSpriteBatch batch;
    privateTextureAtlas atlas;
    privateSprite grassSprite;
 
    publicvoid create() {
        floatscreenW = Gdx.graphics.getWidth();
        floatscreenH = Gdx.graphics.getHeight();
        floatw = 1;
        floath = w * screenH / screenW;
 
        camera = newOrthographicCamera(w, h);
        batch = newSpriteBatch();
 
        atlas = newTextureAtlas("data/grass.pack");
 
        grassSprite = atlas.createSprite("grass");
        grassSprite.setSize(w, w * grassSprite.getHeight() / grassSprite.getWidth());
        grassSprite.setPosition(-w/2, -h/2);
    }
 
    publicvoid render() {
        Gdx.gl.glClearColor(1,1,1,1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
 
        batch.setProjectionMatrix(camera.combined);
        batch.begin();
        grassSprite.draw(batch);
        batch.end();
    }
}

I’m basically creating a sprite that is as wide as the camera viewport, and I position it at the bottom of the viewport. Always work with a viewport that has a fixed width (here: 1 unit), and never work with pixel units. This way, your application will resize itself correctly to every possible resolution, and is not bound to a specific resolution. That’s the most important thing to remember from  this previous article! This simple code sample, once launched, creates the following scene:

What a pure recipe for awesomeness, no? Maybe not. This doesn’t move, so let’s add some wind in those blades!

2. Animating the grass

We want to make our blades move like if wind was pushing them to the left of the screen. If we used one sprite per blade, a rotation of each sprite would fake the effect quite nicely. However, we’re using a single sprite for all the blades, so we can’t rotate it (just try it and you’ll see why). What we want instead is to perform a “skew transformation”. This is a well-known rectangle transformation for those who ever used vector drawings (see following illustration).

To apply this effect, we need to modify the horizontal position of the vertices 2 and 3 of our sprite. Hopefully, LibGDX lets us access the vertices of the sprites with the sprite.getVertices() method! Yay! Only thing we need to do is to animate these vertices in a wave motion. That’s quite easy to do, but we also need to add some randomness to the effect, both in its duration and in its amplitude. And we also want to keep our code quite clean and small. Why not eat my own dog food? The Universal Tween Engine can do all that!

If you ever used the engine with LibGDX, you should already have a TweenAccessor for the Sprite class. This class is responsible for telling the engine how to mess with our objects. What we want to do is just to tell the engine how to apply a “skew transformation”, so we won’t have to dirty our hands with it: the engine will do it for us. Here is how I added support for such transformation to my SpriteAccessor class:

publicclass SpriteAccessor implementsTweenAccessor<Sprite> {
    publicstatic final int SKEW_X2X3 = 1;
 
    @Override
    publicint getValues(Sprite target, inttweenType, float[] returnValues) {
        switch(tweenType) {
            caseSKEW_X2X3:
                float[] vs = target.getVertices();
                returnValues[0] = vs[SpriteBatch.X2] - target.getX();
                returnValues[1] = vs[SpriteBatch.X3] - target.getX() - target.getWidth();
                return2;
        }
 
        assertfalse;
        return-1;
    }
 
    @Override
    publicvoid setValues(Sprite target, inttweenType, float[] newValues) {
        switch(tweenType) {
            caseSKEW_X2X3:
                floatx2 = target.getX();
                floatx3 = x2 + target.getWidth();
                float[] vs = target.getVertices();
                vs[SpriteBatch.X2] = x2 + newValues[0];
                vs[SpriteBatch.X3] = x3 + newValues[1];
                break;
        }
    }
}

Now, only thing left to do is to apply the effect in a wave motion, and repeat indefinitely:

public class AnimatedGrassApp extendsApplicationAdapter {
    privateTweenManager tweenManager = newTweenManager();
    ...
 
    publicvoid create() {
        ...
        Tween.registerAccessor(Sprite.class,newSpriteAccessor());
        Tween.call(windCallback).start(tweenManager);
    }
 
    publicvoid render() {
        tweenManager.update(Gdx.graphics.getDeltaTime());
        ...
    }
 
    privatefinal TweenCallback windCallback = newTweenCallback() {
        @Override
        publicvoid onEvent(inttype, BaseTween<?> source) {
            floatd = MathUtils.random() * 0.5f + 0.5f;  // duration
            floatt = -0.5f * grassSprite.getHeight();    // amplitude
 
            Tween.to(grassSprite, SpriteAccessor.SKEW_X2X3, d)
                .target(t, t)
                .ease(Sine.INOUT)
                .repeatYoyo(1,0)
                .setCallback(windCallback)
                .start(tweenManager);
        }
    };
}

The  windCallback instance fires an animation for one wave motion of the grass. This animation calls this windCallback instance again to relaunch the animation with new random parameters for the duration or the amplitude. This way, the animation loops infinitely, but each time with new random parameters. That’s what we wanted! Result is:

It’s a lot better than the static sprite version, but we can improve it way more!

3. Polishing the effect

This issue with the previous effect is that all blades move at the same speed, that doesn’t feel very natural. To solve this issue, only one way: we need to divide our grass sprite into multiple layers. Therefore, I divided our grass image into three chuncks:

Instead of creating one sprite in the game, I made three of them, each one associated to one of these three grass images. The code is very similar to what we already have. However, what needs to change is the windCallbackinstance. Indeed, I want the three sprites to move like what we did previously, but with a little time offset between each one. Therefore, I went to use a timeline instead of a simple tween this time:

private final TweenCallback windCallback = newTweenCallback() {
    @Override
    publicvoid onEvent(inttype, BaseTween<?> source) {
        floatd = MathUtils.random() * 0.5f + 0.5f;   // duration
        floatt = -0.5f * grassSprite1.getHeight();    // amplitude
 
        Timeline.createParallel()
            .push(Tween.to(grassSprite1, SpriteAccessor.SKEW_X2X3, d).target(t, t).ease(Sine.INOUT).repeatYoyo(1,0).setCallback(windCallback))
            .push(Tween.to(grassSprite2, SpriteAccessor.SKEW_X2X3, d).target(t, t).ease(Sine.INOUT).delay(d/3).repeatYoyo(1,0))
            .push(Tween.to(grassSprite3, SpriteAccessor.SKEW_X2X3, d).target(t, t).ease(Sine.INOUT).delay(d/3*2).repeatYoyo(1,0))
            .start(tweenManager);
    }
};

Basically, the timeline starts three animations at the same time, with a little delay between each one. It is the exact same thing as if I wrote three  Tween.to() animations individually, but is a bit more compact to write (and I’m the lazy type :p). The result is:

Now that’s a lot better! And just imagine how it looks with an animated background and particle effects everywhere!

Conclusion

Of course, there may be many different ways to achieve this effect, but I found this one really easy to implement! Anyway, the only thing to remember is: “if you can’t draw it nicely, make it move!” Especially when it doesn’t involve a lot of work. That’s the kind of thing that can make the difference when people will discover your game.

You can download the eclipse project containing all the sources and images here.


转载:http://www.aurelienribon.com/blog/2012/06/tutorial-animated-grass-using-libgdx/

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值