Monday, June 6, 2016

Iris Wipe Effect tutorial using C, libgdx and Tween engine

Hello and welcome to the this tutorial. There won't be much explanation of the code but instead an explanation on how you can use the code to put an iris wipe effect in your game. If you don't know what an iris wipe is, here is a look at what it does:





The real version is smoother than this video of it.

This effect is similar to old cartoons. When I first wanted to add it to a game, I didn't even know what it was called. I believe I googled "old cartoon black hole" and found it.

This tutorial will assume you are using Eclipse with libgdx set up. It will also need the universal tween engine for libgdx which can be found here This is a great tool to use for all sorts of things and I suggest learning how to use it outside of this tutorial.

There are three classes that we will be using. They are called IrisWipe, IrisWipeManager, and IrisAccessor. IrisWipe is the easiest to understand so let's take a look at it first:

import com.badlogic.gdx.graphics.Color;

public class IrisWipe {

    private float rIn;
    private float centerX;
    private float centerY;
    private Color color; //color of iriswipe (mostly used for the alpha)
    private float timer;
   
    public IrisWipe()
    {
        rIn = 0f;
        centerX = 0f;
        centerY = 0f;
        color = new Color(0f,0f,0f,1f);
        timer = 0f;
    }
   
    public float getTimer() {
        return timer;
    }

    public void setTimer(float timer) {
        this.timer = timer;
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }

    public float getRIn()
    {
        return rIn;
    }
   
    public void setRIn(float rIn)
    {
        this.rIn = rIn;
    }
   
    public float getCenterX()
    {
        return centerX;
    }
   
    public void setCenterX(float centerX)
    {
        this.centerX = centerX;
    }
   
    public float getCenterY()
    {
        return centerY;
    }
   
    public void setCenterY(float centerY)
    {
        this.centerY = centerY;
    }
   
}

This class is used primarily to hold where the center of the wipe will be at. Simply create an instance of it and pass it to an instance of iriswipemanager for which the code is here:



import aurelienribon.tweenengine.BaseTween;
import aurelienribon.tweenengine.Timeline;
import aurelienribon.tweenengine.Tween;
import aurelienribon.tweenengine.TweenCallback;
import aurelienribon.tweenengine.TweenEquations;
import aurelienribon.tweenengine.TweenManager;
import aurelienribon.tweenengine.TweenPaths;

import com.badlogic.gdx.graphics.Color;
import com.kilobolt.gameobjects.IrisWipe;
import com.kilobolt.TweenAccessors.IrisWipeAccessor;
import com.kilobolt.TweenAccessors.Value;
import com.kilobolt.TweenAccessors.ValueAccessor;

public class IrisWipeManager {

    private IrisWipe iriswipe;
    private TweenManager irismanager;
    private boolean wiping; //true when wiping is moving
    private boolean wipingIdle; //true for wipes that have no OUT and just sit there
    private boolean start; //true when time to start game
    private float gameWidth, gameHeight;
   
    //private TweenManager iwmanager;
   
   
    public IrisWipeManager(IrisWipe iriswipe, float gameWidth, float gameHeight)
    {
        this.iriswipe = iriswipe;
        this.gameWidth = gameWidth;
        this.gameHeight = gameHeight;
        wiping = false; //set that no wipes happening to start
        wipingIdle = false;
        start = false; //true when game is to start
        Tween.registerAccessor(IrisWipe.class, new IrisWipeAccessor());
        irismanager = new TweenManager();
    }
   
    public void update(float delta)
    {
        if(wiping | wipingIdle)
        {
            irismanager.update(delta);
        }
    }

    //do iris wipe at a certain area
    //CENTERX AND CENTERY must be set BEFORE CALLING
    //diameter is how big the transparent hole will be
    //IRIS WIPE USAGE
    //position you want - (gameWidth or gameHeight) / 2f
    //iriswipe.setCenterX(x - (gameWidth / 2f));
    //iriswipe.setCenterY(y - (gameHeight / 2f));
    //iriswipemanager.iriswipeSpot(4f);
    public void iriswipeSpot(float diameter, boolean inANDout)
    {
        //kill any previous wipes
        if(irismanager.containsTarget(iriswipe))
        {
            System.out.println("PREVIOUS IRISWIPE KILLED in IWM");
            irismanager.killTarget(iriswipe);
        }

                //callback for no exand out
                TweenCallback cbNOOUT = new TweenCallback() {
                    @Override
                    public void onEvent(int arg0, BaseTween<?> arg1) {
                        //not set to false so that it still draws
                        wiping = false;
                        wipingIdle = true;
                    }
          
                };
       
       
                //shrink callback - expand out now
                TweenCallback cb = new TweenCallback() {
                    @Override
                    public void onEvent(int arg0, BaseTween<?> arg1) {
                       
                        //set wiping to false on final callback
                        TweenCallback cb = new TweenCallback() {
                            @Override
                            public void onEvent(int arg0, BaseTween<?> arg1) {
                                wiping = false;
                                wipingIdle = false;
                            }
                          
                           };
                       
                          
                        //expand wipe out
                        Timeline.createSequence()
                        .push(Tween.to(iriswipe, IrisWipeAccessor.RIN, 3.5f)
                            .target(710f)
                            .ease(TweenEquations.easeInOutCubic))
                            .setCallbackTriggers(TweenCallback.COMPLETE)
                            .setCallback(cb)
                        .start(irismanager);
                    } //this happens after every tween of word ends
                  
                   };
       
        //wipe in
        wiping = true; //wipe is moving
        wipingIdle = false; //wipe is not idling
        if(inANDout) //if it is to go in AND out, set a callback to do the out after in
        {
            Timeline.createSequence()
            .push(Tween.to(iriswipe, IrisWipeAccessor.RIN, 1.0f)
                .target(diameter)
                .ease(TweenEquations.easeOutQuad))
                .setCallbackTriggers(TweenCallback.COMPLETE)
                .setCallback(cb)
            .start(irismanager);
        }else//just have it go IN
        {
            Timeline.createSequence()
            .push(Tween.to(iriswipe, IrisWipeAccessor.RIN, 1.0f)
                .target(diameter)
                .ease(TweenEquations.easeOutQuad))
                .setCallbackTriggers(TweenCallback.COMPLETE)
                .setCallback(cbNOOUT)
            .start(irismanager);
        }
    }
   
   
   
    public void kill()
    {
        irismanager.killAll();
    }
   
    public boolean getWipingIdle() {
        return wipingIdle;
    }

    public void setWipingIdle(boolean wipingIdle) {
        this.wipingIdle = wipingIdle;
    }

    public boolean getWiping()
    {
        return wiping;
    }
   
    public void setWiping(boolean wiping)
    {
        this.wiping = wiping;
    }
   
    public boolean getStart()
    {
        return start;
    }
   
    public void setStart(boolean start)
    {
        this.start = start;
    }
   
}


Some notes about the usage of this class. You have to read the comments above irisWipeSpot() to see how to call it. This class also requires you to know the width and height of your game, which you should know. The parameter diameter that is passed to irisWipeSpot is how big you want the hole to be when it reverses direction. inANDout should be set to true. This can be called from anywhere. The real work occurs in the render method.

Computers apparently handle triangles a lot better than circles. For this reason, we will actually be using triangles. Lots and lots of them. What you are seeing in the gif above is many different triangles that are skinny enough so that it looks smooth like a circle. This part involves quite a bit of trig and the code looks quite ugly. That's the reason I am not going to go into it very much. If you like trig, feel free to dive into the code and have a look. Otherwise, just copy and paste and you'll be good to go. The following code needs to be in your render class. If you have gameRenderer.java or something like that, put the following above your constructor.

 private IrisWipe iriswipe;
    private IrisWipeManager iriswipemanager;
      private float rOut;
      private float rIn;
      private float iriswipecx;
      private float iriswipecy;
      private float iriswipex1;
      private float iriswipey1;
      private float iriswipex2;
      private float iriswipey2;
      private float iriswipex3;
      private float iriswipey3;
      private float iriswipex4;
      private float iriswipey4; 
      private Array<Vector2> iriswipeXY1, iriswipeXY2, iriswipeXY3;
      private Array<Color> iriswipecolor;
      private Value irisSwipeValue;
     private boolean irisswipe, irisswipeFirstTime;

That's a lot of variables. I told you to just copy and paste. Now in the constructor, put this:

         batcher = new SpriteBatch();
        // Attach batcher to camera
        batcher.setProjectionMatrix(cam.combined);
       
        shapeRenderer = new ShapeRenderer();
        shapeRenderer.setProjectionMatrix(cam.combined);
       
        rOut = gameHeight * 2f; //something big enough to cover whole screen
        rIn = 0f;//irisSwipeValue.getValue();
        iriswipecx = screenWidth / 2f;
        iriswipecy = gameHeight / 2f;
        irisswipe = true;
        irisswipeFirstTime = true;
        irisSwipeValue = new Value();
        irisSwipeValue.setValue(0f);
       
        iriswipex1 = 0f;;
        iriswipey1 = 0f;
        iriswipex2 = 0f;
        iriswipey2 = 0f;
        iriswipex3 = 0f;
        iriswipey3 = 0f;
        iriswipex4 = 0f;
        iriswipey4 = 0f;
       
        iriswipeXY1 = new Array<Vector2>();
        iriswipeXY2 = new Array<Vector2>();
        iriswipeXY3 = new Array<Vector2>();
        iriswipecolor = new Array<Color>();
        int iIris = 0;
       
        while(iIris <= 361)
        {
           
            iriswipeXY1.add(new Vector2((float)Math.cos(Math.toRadians((double)((iIris)))), (float)Math.sin(Math.toRadians((double)(iIris)))));
            iriswipeXY2.add(new Vector2((float)Math.cos(Math.toRadians((double)((iIris)))), (float)Math.sin(Math.toRadians((double)(iIris)))));
            iriswipeXY3.add(new Vector2((float)Math.cos(Math.toRadians((double)((iIris + 1f)))), (float)Math.sin(Math.toRadians((double)((iIris + 1f))))));
                     
            iIris++;
        }



If you already have a Spritebatch and/or shaperenderer you won't need the top part of the code. The mathy parts at the bottom are beyond what I will explain here. If you understand trig enough, you can get a sense of what these arrays are filling up with. Now in your game loop put:

drawIrisWipe();

And that's it! Wait, we need the code for the method above. This isn't going to look pretty:

 private void drawIrisWipe()
    {
        //if iriswipe is not moving and not idle either, dont draw
        if(!iriswipemanager.getWiping() & !iriswipemanager.getWipingIdle())
        {
            return;
        }
        shapeRenderer.begin(ShapeType.Filled);
        shapeRenderer.setColor(iriswipe.getColor());
        rIn =  iriswipe.getRIn();  //tweened value of rIn
       
        int degree = 0;
        while(degree < 360)
        {
            iriswipex1 = (rOut * iriswipeXY1.get(degree).x) + iriswipecx + iriswipe.getCenterX();
            iriswipey1 = (rOut * iriswipeXY1.get(degree).y) + iriswipecy + iriswipe.getCenterY();
            iriswipex2 = (rIn * iriswipeXY2.get(degree).x) + iriswipecx + iriswipe.getCenterX();
            iriswipey2 = (rIn * iriswipeXY2.get(degree).y) + iriswipecy + iriswipe.getCenterY();
            iriswipex3 = (rIn * iriswipeXY3.get(degree).x) + iriswipecx + iriswipe.getCenterX();
            iriswipey3 = (rIn * iriswipeXY3.get(degree).y) + iriswipecy + iriswipe.getCenterY();
            iriswipex4 = (rOut * iriswipeXY1.get(degree + 1).x) + iriswipecx + iriswipe.getCenterX();
            iriswipey4 = (rOut * iriswipeXY1.get(degree + 1).y) + iriswipecy + iriswipe.getCenterY();
           
                shapeRenderer.triangle(iriswipex1, iriswipey1, iriswipex2, iriswipey2, iriswipex3, iriswipey3);
                shapeRenderer.triangle(iriswipex1, iriswipey1, iriswipex4, iriswipey4, iriswipex3, iriswipey3);
           
           
            degree += 1;
        }
        shapeRenderer.end();
    }


I originally added this to a game I made, but I've added it now to my path finding program just as a demo. If you put in all the above code and call irismanager.update(delta) in your game loop, it should work fine. If you want to look at it in the path finding program, you can download that here

Hopefully this is helpful. There were no tutorials on how to make this effect online so if it is a little rough around the edges, fix it and tell me what you did! Comment if you need help

No comments:

Post a Comment