Search code examples
javalibgdxgame-physics

how to Smooth catmull rom spline curve when it is moving like amaging wire in libgdx


i have tried to move my curve but it is not moving well, when it changes its direction from left to right or right to left then the movement is quite awkward. i want to move my curve like this video video of curve movement what i actually want. In this video when a it change its direction it is so graceful but in my case it change its direction and the curve gives a crazy shape at newly added point. Experts please solve this problem. here is my code

//create paths
private Bezier<Vector2> path1; 
private CatmullRomSpline<Vector2> path2;
private ShapeRenderer sr;
int height,width;
Vector2 starting,ending,endingControl;
ArrayList<Vector2> listOfPoints;
Vector3 touchPos;
float timeDifference;
Boolean leftPos=false,rightPos=false;
Boolean isTouch=false,isTouchUp=false;
Vector2 mVector2;
private OrthographicCamera cam;
Vector2[] controlPoints;

@Override
public void create () { 

     width = Gdx.graphics.getWidth();
     height = Gdx.graphics.getHeight();
     ending=new Vector2(width/2,height/2);
     endingControl=new Vector2(ending.x,ending.y+10);
     starting=new Vector2(width/2,0);

    controlPoints = new Vector2[]{starting,starting,ending,ending};



    // set up the curves

    path2 = new CatmullRomSpline<Vector2>(controlPoints, false);
    listOfPoints=new ArrayList<Vector2>();
    // setup ShapeRenderer
    sr = new ShapeRenderer();
    sr.setAutoShapeType(true);
    sr.setColor(Color.BLACK);
    cam=new OrthographicCamera();
    cam.setToOrtho(false);
    listOfPoints.add(new Vector2(width/2,0)); //starting 
    listOfPoints.add(new Vector2(width/2,0)); //starting

}

@Override
public void resize(int width, int height) {
    // TODO Auto-generated method stub
    super.resize(width, height);


    cam.update();
}

@Override
public void render () {
    cam.update();
    Gdx.gl.glClearColor(1f, 1f, 1f, 1f);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

    sr.begin();
    sr.set(ShapeType.Filled);


    if(Gdx.input.isTouched())
    {


        if(!isTouch){
            listOfPoints.add(new Vector2(ending.x+2, ending.y-4));


            int s=listOfPoints.size();
            controlPoints=new Vector2[s+2];
            listOfPoints.toArray(controlPoints);


            controlPoints[s]=ending;
            //endingControl.x=ending.y;
            controlPoints[s+1]=ending;
            path2 = new CatmullRomSpline<Vector2>(controlPoints, false);

        }

        isTouch=true;
        ending.x+=3;





    }
    else {

        if(isTouch){
            listOfPoints.add(new Vector2(ending.x-2, ending.y-4));
           int s=listOfPoints.size();
            controlPoints=new Vector2[s+2];
            listOfPoints.toArray(controlPoints);


            controlPoints[s]=ending;

            controlPoints[s+1]=ending;

            path2 = new CatmullRomSpline<Vector2>(controlPoints, false);


        }
        isTouch=false;
        ending.x-=3;

    }


    moveAndReduce();


    for(int i = 0; i < 100; ++i){
        float t = i /100f;
        Vector2 st = new Vector2();
        Vector2 end = new Vector2();
        path2.valueAt(st,t);
        path2.valueAt(end, t-0.01f);
        sr.rectLine(st.x, st.y, end.x, end.y,3);

    }

    sr.end();

}

@Override
public void dispose () {
    sr.dispose();
}

public void moveAndReduce()
{
    for(Vector2 vector2:listOfPoints)
    {
        vector2.y-=3 ;

    }
    if(listOfPoints.size()>3 && listOfPoints.get(3).y<-1)
    {

        listOfPoints.remove(0);
        listOfPoints.set(0, listOfPoints.get(1));
        int s=listOfPoints.size();
        controlPoints=new Vector2[s+2];
        listOfPoints.toArray(controlPoints);
        controlPoints[s]=ending;
        controlPoints[s+1]=ending;    
        path2 = new CatmullRomSpline<Vector2>(controlPoints, false);    
    }
}

Solution

  • Going by the video the curve does not look like it is constrained by control points but just a simple trace of and accelerating point.

    You create an array of floats the length in pixels matching the length of the line in the x direction. For example if screen is 200 pixels wide the line can be 100 so the array is 100 in length. Set each float in the array to the start value half the screen height. I call the array line in this answer. You call it what you like.

    The you assign a head index that is the index of the rightmost point. Each frame you move the head index up by one. If it is over the array length-1 you set it to zero (beginning of array)

    Rendering the Path

    When you draw the line you draw all the points from head + 1

    Path p = new Path();
    for(int i = 0; i < 100; ++i){
       p.lineTo(i, line[(i + head + 1) % 100]); // add path points
    }
    // draw the path;
    

    Moving up and down

    To make it move you have a movement float move that is 0 for no movement or positive and negative values the move up or down.

    When you want it to move increase the move amount by a fixed value.

    // moving down
    if(move < maxMove){  // set a max move amount eg 10
        move += moveAmount;  // moveAmount 0.2 just as an example
    }
    

    Same for moving up, but subtract

    When there is no input you move the move amount back to zero by a fixed rate

    // assume this is code run when no input
    if(move != 0){
        if(Math.abs(move) < moveAmount){ // if close to zero set to zero
           move = 0;
        }else{
           move -= Math.sign(move) * moveAmount; // else move towards zero at
                                                 // fixed rate
        }
    }
    

    Moving forward

    The line does not move forward, just appears to do so as we move the head position up the array each frame.

    Back to moving the line's head the following move the line head position up or down (but is not complete the last line is modified to create a smoother curve)

    float pos = line[head]; // get the pos of line at head
    head += 1; // move the head forward 1
    head %= 100; // if past end of array move to 0
    line[head] = pos + move; // set the new head position
    

    A better curve

    This will move the head of the line up or down depending on move. The curve we get is not that nice so to make it a little smoother you need to change the rate the move value changes the head position.

    // an sCurve for any value of move the result is  from -1 to 1
    // the greater or smaller move the closer to 1 or -1 the value gets
    // the value -1.2 controls the rate at which the value moves to 1 or -1
    // the closer to -1 the value is the slower the value moves to 1 or -1    
    float res =  (2 / (1 + Math.pow(move,-1.2))) -1;
    

    This in effect changes the shape of the lines curve to a almost sine wave when moving up and down

    // so instead of
    //line[head] = pos + move; // set the new head position
    line[head] = pos + ( (2 / (1 + Math.pow(move,-1.2))) -1 ) * maxSpeed;
    // max speed is the max speed the line head can move up or down
    // per frame in pixels.
    

    Example to show the curve

    Below is a Javascript implementation that does it as outlined above (is not intended as answer code). Use the keyboard Arrow Up and Arrow down to move the line

    If you are using a tablet or phone then the following image is what you will see as way to late for me to add and test touch for the example

    enter image description here

    const doFor = (count, callback) => {var i = 0; while (i < count) { callback(i ++) } };
    const keys = {
        ArrowUp : false,
        ArrowDown : false,
    };
    function keyEvents(e){
        if(keys[e.code] !== undefined){
            keys[e.code] = event.type === "keydown";
            e.preventDefault();
        }
    }
    addEventListener("keyup", keyEvents);
    addEventListener("keydown", keyEvents);
    focus();
    var gameOver = 0;
    var gameOverWait = 100;
    var score = 0;
    var nextWallIn = 500
    var nextWallCount = nextWallIn;
    var wallHole = 50;
    const wallWidth = 5;
    const walls = [];
    function addWall(){
      var y = (Math.random() * (H - wallHole  * 2)) + wallHole *0.5;
      walls.push({
          x : W,
          top : y,
          bottom : y + wallHole,
          point : 1, // score point
      });
    }
    function updateWalls(){
       nextWallCount += 1;
       if(nextWallCount >= nextWallIn){
          addWall();
          nextWallCount = 0;
          nextWallIn -= 1;
          wallHole -= 1;
       }
       for(var i = 0; i < walls.length; i ++){
          var w = walls[i];
          w.x -= 1;
          if(w.x < -wallWidth){
            walls.splice(i--,1);
          }
          if(w.x >= line.length- + wallWidth && w.x < line.length){
              var pos = line[head];
              if(pos < w.top || pos > w.bottom){
                  gameOver = gameOverWait;
              }
          }
          if(w.point > 0 && w.x <= line.length){
            score += w.point;
            w.point = 0;
          }
       }
    }
    function drawWalls(){
      for(var i = 0; i < walls.length; i ++){
          var w = walls[i];
          ctx.fillStyle = "red";
          ctx.fillRect(w.x,0,wallWidth,w.top);
          ctx.fillRect(w.x,w.bottom,wallWidth,H-w.bottom);
      }
    }
    const sCurve = (x,p) =>  (2 / (1 + Math.pow(p,-x))) -1;
    const ctx = canvas.getContext("2d");
    var W,H; // canvas width and height
    const line = [];
    var move = 0;
    var curvePower = 1.2;
    var curveSpeed = 0.2;
    var maxSpeed = 10;
    var headMoveMultiply = 2;
    var head;
    function init(){
      line.length = 0;
      doFor(W / 2,i => line[i] = H / 2);
      head = line.length - 1;
      move = 0;
      walls.length = 0;
      score = 0;
      nextWallIn = 500
      nextWallCount = nextWallIn;
      wallHole = 50;
      ctx.font = "30px arial black";
    }
    function stepLine(){
      var pos = line[head];
      head += 1;
      head %= line.length;
      
      line[head] = pos + sCurve(move,curvePower)*headMoveMultiply ;
    
    }
    function drawLine(){
      ctx.beginPath();
      ctx.strokeStyle = "black";
      ctx.lineWidth = 3;
      ctx.lineJoin = "round";
      ctx.lineCap = "round";
      for(var i = 0; i <line.length; i++){
        ctx.lineTo(i,line[(i + head + 1) % line.length]);
      }
      ctx.stroke();
    }
    
    function mainLoop(time){
        if(canvas.width !== innerWidth || canvas.height !== innerHeight){ 
            W = canvas.width = innerWidth;
            H = canvas.height = innerHeight;
            init();
        }
        if(gameOver === 1){
          gameOver = 0;
          init();
        }
        ctx.setTransform(1,0,0,1,0,0); 
        ctx.clearRect(0,0,W,H); 
        if(keys.ArrowUp){
            if(move > - maxSpeed){
              move -= curveSpeed;
            }
            
        }else if(keys.ArrowDown){
            if(move < maxSpeed){
               move += curveSpeed;
            }
        }else{
            move -= Math.sign(move)*curveSpeed;
            if(Math.abs(move) < curveSpeed){
                move = 0;
            }
        }
        if(gameOver === 0){
            stepLine();
            updateWalls();
        }
        drawLine();
        drawWalls();
        ctx.fillStyle = "Black";
        ctx.textAlign = "left";
        ctx.fillText("Score : " + score, 10,30);
        if(gameOver > 0){
            ctx.textAlign = "center";
            ctx.fillText("Crashed !!", W / 2,H * 0.4);
            gameOver -= 1;
        }
        requestAnimationFrame(mainLoop);
    }
    requestAnimationFrame(mainLoop);
    canvas {
        position : absolute;
        top : 0px;
        left : 0px;
        z-index : -10;
    }
    <br><br><br>Up down arrow keys to move line.
    <canvas id=canvas></canvas>