如何模拟链物理(游戏设计)

时间:2017-03-05 14:03:02

标签: actionscript-3 game-physics

我正在尝试为游戏创建一系列移动对象(AS3)。到目前为止,我可以将对象拖到另一个对象后面,但我所能做的就是根据距离使链接更接近另一个链接。它没有现实地工作,它仍然只遵循一个方向。如果我试图将链条拉向相反的方向,它就不起作用。

我需要一个公式来拉动任何链接后面的链。

我还需要一个公式,让链接在静止时落在链的其余部分之下。我现在设置它的方式,链接试图下降,但它们只是直接下降而不是被拉到链的其余部分之下。

有一些链和字符串教程,但它们似乎都没有引入重力或两种方式。

enter image description here

2 个答案:

答案 0 :(得分:9)

Verlet模拟

你最好的选择是verlet链仿真。它结合了重力,约束,并且可以在任何点施加力。

在verlet集成中(将数学分为几个步骤),您只存储顶点的当前位置和前一帧中的位置。与存储当前位置和当前速度和方向的欧几里德积分不同,微粒积分将当前速度和方向存储为位置之间的差异。这非常适合您所处的模拟类型,其中复杂的交互不必担心动量,旋转和许多其他问题。你要做的只是记住最后一个位置并设置新的,其他一切都会自动发生。

我将使用javascript表示法,因为我从未使用过动作脚本。

基础知识。

sim从点(顶点)开始。顶点是没有大小的自由点。它每帧都有重力,它受到地面等环境对象的约束。

存储顶点所需的信息

vertex = {   
    x : 100, // current position
    y : 100,
    lx : 100,  // last position This point is not moving hence the last pos 
    ly : 100,  // is the same as the current
}

对于动画的每个帧,您将各种力应用于顶点

移动点功能和一些常量

GRAV = 0.9;          // force of gravity
GROUND = 400;        // Y position of the ground. Y is down the screen
GROUND_BOUNCE = 0.5; // Amount of bounce from the ground
DRAG = 0.9;          // Amount of friction or drag to apply per frame

dx = (vertex.x-vertex.lx) * DRAG;  // get the speed and direction as a vector 
dy = (vertex.y-vertex.ly) * DRAG;  // including drag
vertex.lx = vertex.x;   // set the last position to the current
vertex.ly = vertex.y;
vertex.x += dx;         // add the current movement
vertex.y += dy;  
vertex.y += GRAV;       // add the gravity

每个帧还需要应用约束。它们几乎可以是任何东西。

现在这只是基础。如果位置大于地线,则将点移离地面。该点也可以是固定的(不需要施加上述移动阻力和重力)或固定到移动物体,如鼠标。

约束地线。

因为最后一个位置(vertex.lxvertex.ly)有助于定义当前的移动。当我们改变方向(击中地面)时,我们必须改变最后位置以正确描述新方向,这将最后位置置于地下。

限制点功能

if(vertex.y > GROUND){
    // we need the current speed
    dx = (vertex.x-vertex.lx) * DRAG;
    dy = (vertex.y-vertex.ly) * DRAG;
    speed = sqrt(dx*dx+dy*dy);
    vertex.y = GROUND;  // put the current y on the ground
    vertex.ly = GROUND + dy * GROUND_BOUNCE; // set the last point into the 
                                             // ground and reduce the distance 
                                             // to account for bounce
    vertex.lx += (dy / speed) * vx; // depending on the angle of contact
                                    // reduce the change in x and set 
                                    // the new last x position
 }

一堆观点。

这是引力,空气摩擦和约束的基本数学,如果我们创建一个点并应用数学,它将落到地面,反弹几次并停止。

因为我们需要很多顶点,所以你需要为这个答案points创建一个数组。

现在是时候连接这些点并将一堆自由浮动顶点转换成各种各样的模拟结构。

线条约束

对于这个答案,该行表示链中的链接。

enter image description here

上图是为了帮助想象这个概念。 A和B是两个顶点,它们旁边的红点是顶点的最后位置。红色箭头显示线条约束将移动顶点的近似方向。线的长度是固定的,下面的算法试图找到最佳解决方案,使所有线尽可能接近这个长度。

描述一行

line = {
    pointIndex1 : 0,  // array index of connected point
    pointIndex2 : 1,  // array index of second connected point
    length : 100,  // the length of the line.
    // in the demo below I also include a image index and
    // a draw function 
}

一条线连接两个顶点。顶点可以有许多连接到它的线。

在我们应用移动,拖动,重力和任何其他约束之后的每个帧我们应用线约束。从上图中可以看出,两个顶点的最后位置位于距离线太远的位置,无法连接线。为了固定线,我们将两个顶点移动到线的中心点(红色箭头)。如果这两个点距离线长太近,我们会将这些点移开。

以下是用于执行此操作的数学计算。

限制线功能

p1 = points[line.pointIndex1]; // get first point
p2 = points[line.pointIndex2]; // get second point
// get the distance between the points
dx = p2.x - p1.x;
dy = p2.y - p1.y;
distance = sqrt(dx * dx + dy * dy);
// get the fractional distance the points need to move toward or away from center of 
// line to make line length correct
fraction = ((line.length - distance) / distance) / 2;  // divide by 2 as each point moves half the distance to 
                                                       // correct the line length
dx *= fraction;  // convert that fraction to actual amount of movement needed
dy *= fraction;
p1.x -=dx;   // move first point to the position to correct the line length
p1.y -=dy;
p2.x +=dx;   // move the second point in the opposite direction to do the same.
p2.y +=dy;

这个约束很简单,因为我们使用的是verlet集成,所以我们不必担心点或线的速度和方向。最重要的是,我们不必处理任何轮换,因为这也需要处理。

很多点,很多行

此时我们已经完成了单行所需的所有数学运算,我们可以添加更多行,将第一行的终点连接到第二行的开头,依此类推,无论多长时间我们都需要链。一旦我们连接了所有点,我们将标准约束应用于所有点,然后逐个应用线约束。

这是我们遇到一个小问题的地方。当我们移动点以校正第一行的长度时,我们移动下一行的起点,然后当我们移动下一行的点时,我们移动第一行的端点,打破其长度约束。当我们遍历所有线路时,唯一具有正确长度的线路将是最后一条线路。所有其他行将被略微拉向最后一行。

我们可以保持原样并且这会给链条带来一些弹性的感觉(在这种情况下不合需要但是对于鞭子和绳索来说效果很好),我们可以为线段做一个完整的反向运动学终点解决方案(方式努力)或我们可以作弊。如果再次应用线条约束,则将所有点更多地移向正确的解决方案。我们一次又一次地这样做,纠正了前一次传递中引入的错误。

这个迭代过程将朝着一个解决方案发展,但它永远不会是完美的,但我们可以快速找到错误在视觉上无法察觉的情况。为方便起见,我喜欢将迭代次数称为sim的刚度。值为1表示线条是弹性的,值为10表示线条几乎没有明显的拉伸。

注意,链条越长,拉伸变得越明显,因此需要进行的迭代越多,以获得所需的刚度。

注意这种寻找解决方案的方法存在缺陷。在许多情况下,点和线的排列有多种解决方案。如果系统中存在大量移动,则可能在两个(或更多)可能的解决方案之间开始振荡。如果发生这种情况,您应该限制用户可以添加到系统的移动量。

主循环。

将所有这些放在一起我们需要每帧运行一次sim。我们有一个点阵列,以及连接点的一系列线。我们移动所有点,然后我们应用线约束。然后渲染结果准备好再次进行下一帧。

 STIFFNESS = 10; // how much the lines resist stretching
 points.forEach(point => { // for each point in the sim
       move(point); // add gravity drag 
       constrainGround(point); // stop it from going through the ground line
 })
 for(i = 0; i < STIFFNESS; i+= 1){  // number of times to apply line constraint
     lines.forEach(line => { // for each line in the sim
          constrainLine(line);
     })
 }
 drawPoints(); // draw all the points
 drawLines(); // draw all the lines.

问题特定问题

上述方法提供了很好的线条模拟,它还可以制作刚体,布娃娃,桥梁,盒子,牛,山羊,猫和狗。通过固定点,您可以做各种悬挂绳索和链条,创建滑轮,坦克踏板。非常适合模拟2D汽车和自行车。但请记住,它们在视觉上都是可以接受的,但根本不是真正的物理模拟。

你想要一个链。要制作链条,我们需要给线条一些宽度。因此每个顶点都需要一个半径,地面约束需要考虑到这一点。我们还希望链不会落在自身上,因此我们需要一个新的约束来防止球(AKA顶点)相互重叠。这将为sim增加额外的负荷,因为每个球需要相互测试球,并且当我们调整位置以停止重叠时,我们会在线长度上添加误差,因此我们需要依次多次执行每个约束。每帧都能得到一个好的解决方案。

最后一部分是图形的细节,每一行都需要引用一个可视化表示链的图像。

我会把所有这些都留给你来弄清楚如何在动作中做得最好。

演示

以下演示显示了上述所有内容的结果。它可能不是您想要的,还有其他方法可以解决问题。这种方法有几个问题

  • 离散质量,质量由点定义,你不能在没有更多数学的情况下使点更轻或更重
  • 摆动状态。有时系统会开始振荡,试图将运动保持在合理的水平。
  • 拉伸。尽管可以几乎消除拉伸,但在某些情况下解决方案并不完美。像松紧带一样,如果拉伸链松开,它会轻弹。我没有增加迭代次数以匹配链长,所以较长的链将显示拉伸,如果你摆动它,你将看到链分开。

您感兴趣的功能包括constrainPointconstrainLinemovePointdoSim(只是runSim中if(points.length > 0){之后的位)所有休息只是支持和样板。

最佳视图为完整页面(我使图像有点太大oops ... :(

要查看链单击并按住鼠标右键添加第一个块,然后添加链接到链。我没有限制链条的长度。单击并按住左按钮以抓取并拖动链的任何部分并阻止。

&#13;
&#13;
var points = [];
var lines = [];
var pointsStart;
var fric = 0.999; // drag or air friction
var surF = 0.999; // ground and box friction
var grav = 0.9;   // gravity
var ballRad = 10;  // chain radius set as ball radius
var stiffness = 12;  // number of itterations for line constraint
const fontSize = 33;
var chainImages = [new Image(),new Image(),new Image()];
chainImages[0].src = "https://i.stack.imgur.com/m0xqQ.png";
chainImages[1].src = "https://i.stack.imgur.com/fv77t.png";
chainImages[2].src = "https://i.stack.imgur.com/tVSqL.png";

// add a point
function addPoint(x,y,vx,vy,rad = 10,fixed = false){
    points.push({
        x:x,
        y:y,
        ox:x-vx,
        oy:y-vy,
        fixed : fixed,
        radius : rad,
    })
    return points[points.length-1];
}
// add a constrained line
function addLine(p1,p2,image){
    lines.push({
        p1,p2,image,
        len : Math.hypot(p1.x - p2.x,p1.y-p2.y),
        draw(){
            if(this.image !== undefined){
                var img = chainImages[this.image];
                var xdx = this.p2.x - this.p1.x;
                var xdy = this.p2.y - this.p1.y;
                var len = Math.hypot(xdx,xdy);
                xdx /= len;
                xdy /= len;
                if(this.image === 2){ // oops block drawn in wrong direction. Fix just rotate here
                                      // also did not like the placement of 
                                      // the block so this line's image
                                      // is centered on the lines endpoint
                    ctx.setTransform(xdx,xdy,-xdy,xdx,this.p2.x, this.p2.y);

                    ctx.rotate(-Math.PI /2);
                }else{
                    ctx.setTransform(xdx,xdy,-xdy,xdx,(this.p1.x + this.p2.x)/2,(this.p1.y + this.p2.y)/2);
                }
                ctx.drawImage(img,-img.width /2,- img.height / 2);
            }
        }
    })   
    return lines[lines.length-1];
}
// Constrain a point to the edge of the canvas
function constrainPoint(p){
    if(p.fixed){
        return;
    }
    var vx = (p.x - p.ox) * fric;
    var vy = (p.y - p.oy) * fric;
    var len = Math.hypot(vx,vy);
    var r = p.radius;
    if(p.y <= r){
        p.y = r;
        p.oy = r + vy * surF;
    }
    if(p.y >= h - r){
        var c = vy / len 
        p.y = h - r
        p.oy = h - r + vy * surF;
        p.ox += c * vx;
    }
    if(p.x < r){
        p.x = r;
        p.ox = r + vx * surF;
    }
    if(p.x > w - r){
        p.x = w - r;
        p.ox = w - r + vx * surF;
    }
}
// move a point 
function movePoint(p){
    if(p.fixed){
        return;
    }
    var vx = (p.x - p.ox) * fric;
    var vy = (p.y - p.oy) * fric;
    p.ox = p.x;
    p.oy = p.y;
    p.x += vx;
    p.y += vy;
    p.y += grav;
}
// move a line's end points constrain the points to the lines length
function constrainLine(l){
    var dx = l.p2.x - l.p1.x;
    var dy = l.p2.y - l.p1.y;
    var ll = Math.hypot(dx,dy);
    var fr = ((l.len - ll) / ll) / 2;
    dx *= fr;
    dy *= fr;
    if(l.p2.fixed){
        if(!l.p1.fixed){
            l.p1.x -=dx * 2;
            l.p1.y -=dy * 2;
        }
    }else if(l.p1.fixed){
        if(!l.p2.fixed){
            l.p2.x +=dx * 2;
            l.p2.y +=dy * 2;
        }
    }else{
        l.p1.x -=dx;
        l.p1.y -=dy;
        l.p2.x +=dx;
        l.p2.y +=dy;
    }
}
// locate the poitn closest to x,y (used for editing)
function closestPoint(x,y){
    var min = 40;
    var index = -2;
    for(var i = 0; i < points.length; i ++){
        var p = points[i];
        var dist = Math.hypot(p.x-x,p.y-y);
        p.mouseDist = dist;
        if(dist < min){
            min = dist;
            index = i;
            
        }
        
    }
    return index;
}

function constrainPoints(){
    for(var i = 0; i < points.length; i ++){
        constrainPoint(points[i]);
    }
}
function movePoints(){
    for(var i = 0; i < points.length; i ++){
        movePoint(points[i]);
    }
}
function constrainLines(){
    for(var i = 0; i < lines.length; i ++){
        constrainLine(lines[i]);
    }
}
function drawLines(){
    // draw back images first
    for(var i = 0; i < lines.length; i ++){
        if(lines[i].image !== 1){
            lines[i].draw();
        }
    }
    for(var i = 0; i < lines.length; i ++){
        if(lines[i].image === 1){
            lines[i].draw();
        }
    }
}
// Adds the block at end of chain
function createBlock(x,y){
    var i = chainImages[2];
    var w = i.width;
    var h = i.height;
    var p1 = addPoint(x,y+16,0,0,8);
    var p2 = addPoint(x-w/2,y+27,0,0,1);
    var p3 = addPoint(x+w/2,y+27,0,0,1);
    var p4 = addPoint(x+w/2,y+h,0,0,1);
    var p5 = addPoint(x-w/2,y+h,0,0,1);
    var p6 = addPoint(x,y+h/2,0,0,1);
    addLine(p1,p2);
    addLine(p1,p3);
    addLine(p1,p4);
    addLine(p1,p5);
    addLine(p1,p6,2);
    addLine(p2,p3);
    addLine(p2,p4);
    addLine(p2,p5);
    addLine(p2,p6);
    addLine(p3,p4);
    addLine(p3,p5);
    addLine(p3,p6);
    addLine(p4,p5);
    addLine(p4,p6);
    addLine(p5,p6);
    var p7 = addPoint(x,y + 16-(chainImages[0].width-ballRad * 2),0,0,ballRad);
    addLine(p1,p7,1);
}
var lastChainLink = 0;
function addChainLink(){
    var lp = points[points.length-1];
    addPoint(lp.x,lp.y-(chainImages[0].width-ballRad*2),0,0,ballRad);
    addLine(points[points.length-2],points[points.length-1],lastChainLink % 2);
    lastChainLink += 1;
}
    
function loading(){
    ctx.setTransform(1,0,0,1,0,0)    
    ctx.clearRect(0,0,w,h);
    ctx.fillStyle = "black";
    ctx.fillText("Loading media pleaase wait!!",w/2,30);
    if(chainImages.every(image=>image.complete)){
        doSim = runSim;
    }
}
var onResize = function(){ // called from boilerplate
  blockAttached = false;
  lines.length = 0;  // remove all lines and points.
  points.length = 0; 
  lastChainLink = 0; // controls which chain image to use next
  holdingCount = 0;
  holding = -1;
  mouse.buttonRaw = 0;
}
var blockAttached = false;
var linkAddSpeed = 20;
var linkAddCount = 0;
var holding = -1; // the index of the link the mouse has grabbed
var holdingCount = 0;
function runSim(){
    ctx.setTransform(1,0,0,1,0,0)    
    ctx.clearRect(0,0,w,h);
    ctx.fillStyle = "black";
    if(points.length < 12){
        ctx.fillText("Right mouse button click hold to add chain.",w/2,30);
    }
    if(holdingCount < 180){
        if(mouse.buttonRaw & 1 && holding === -2){
            ctx.fillText("Nothing to grab here.",w/2,66);
        }else{
            ctx.fillText("Left mouse button to grab and move chain.",w/2,66);
        }
    }
    if(mouse.buttonRaw & 4){
        if(linkAddCount > 0){  // delay adding links
            linkAddCount-=1;
        }else{
            if(!blockAttached ){
                createBlock(mouse.x,mouse.y)
                blockAttached = true;
            }else{
                addChainLink(mouse.x,mouse.y);
            }
            linkAddCount = linkAddSpeed;
        }
    }
    if(points.length > 0){
        if(mouse.buttonRaw & 1){
            if(holding < 0){
                holding = closestPoint(mouse.x,mouse.y);
            }
        }else{
            holding = -1;
        }
        movePoints();
        constrainPoints();
        // attach the last link to the mouse
        if(holding > -1){
            var mousehold = points[holding];
            mousehold.ox = mousehold.x = mouse.x;
            mousehold.oy = mousehold.y = mouse.y;
            holdingCount += 1; // used to hide help;
        }
        
        for(var i = 0; i < stiffness; i++){
            constrainLines();
            if(holding > -1){
                mousehold.ox = mousehold.x = mouse.x;
                mousehold.oy = mousehold.y = mouse.y;
            }
        }
        drawLines();
    }else{
        holding = -1;
    }
}

var doSim = loading;

/*********************************************************************************************/
/* Boilerplate not part of answer from here down */
/*********************************************************************************************/
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
function start(x,y,col,w){ctx.lineWidth = w;ctx.strokeStyle = col;ctx.beginPath();ctx.moveTo(x,y)}
function line(x,y){ctx.lineTo(x,y)}
function end(){ctx.stroke()} 
function drawLine(l) {ctx.lineWidth = 1;ctx.strokeStyle = "Black";ctx.beginPath();ctx.moveTo(l.p1.x,l.p1.y);ctx.lineTo(l.p2.x,l.p2.y); ctx.stroke();}
function drawPoint(p,col = "black", size = 3){ctx.fillStyle = col;ctx.beginPath();ctx.arc(p.x,p.y,size,0,Math.PI * 2);ctx.fill();}

;(function(){
    const RESIZE_DEBOUNCE_TIME = 100;
    var  createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
    createCanvas = function () {
        var c, cs;
        cs = (c = document.createElement("canvas")).style;
        cs.position = "absolute";
        cs.top = cs.left = "0px";
        cs.zIndex = 1000;
        document.body.appendChild(c);
        return c;
    }
    resizeCanvas = function () {
        if (canvas === undefined) {
            canvas = createCanvas();
        }
        canvas.width = innerWidth;
        canvas.height = innerHeight;
        ctx = canvas.getContext("2d");
        if (typeof setGlobals === "function") {
            setGlobals();
        }
        if (typeof onResize === "function") {
            if(firstRun){
                onResize();
                firstRun = false;
            }else{
                resizeCount += 1;
                setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
            }
        }
    }
    function debounceResize() {
        resizeCount -= 1;
        if (resizeCount <= 0) {
            onResize();
        }
    }
    setGlobals = function () {
        cw = (w = canvas.width) / 2;
        ch = (h = canvas.height) / 2;
        ctx.font = fontSize + "px arial";
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        
    }
    mouse = (function () {
        function preventDefault(e) {
            e.preventDefault();
        }
        var mouse = {
            x : 0,
            y : 0,
            w : 0,
            buttonRaw : 0,
            over : false,
            bm : [1, 2, 4, 6, 5, 3],
            active : false,
            bounds : null,
            mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
        };
        var m = mouse;
        function mouseMove(e) {
            var t = e.type;
            m.bounds = m.element.getBoundingClientRect();
            m.x = e.pageX - m.bounds.left;
            m.y = e.pageY - m.bounds.top;
            if (t === "mousedown") {
                m.buttonRaw |= m.bm[e.which - 1];
            } else if (t === "mouseup") {
                m.buttonRaw &= m.bm[e.which + 2];
            } else if (t === "mouseout") {
                m.buttonRaw = 0;
                m.over = false;
            } else if (t === "mouseover") {
                m.over = true;
            } else if (t === "mousewheel") {
                m.w = e.wheelDelta;
            } else if (t === "DOMMouseScroll") {
                m.w = -e.detail;
            }
            e.preventDefault();
        }
        m.start = function (element) {
            m.element = element === undefined ? document : element;
            m.mouseEvents.forEach(n => {
                m.element.addEventListener(n, mouseMove);
            });
            m.element.addEventListener("contextmenu", preventDefault, false);
            m.active = true;
        }
        return mouse;
    })();

    function update(timer) { // Main update loop
        doSim(); // call demo code
        requestAnimationFrame(update);
    }
    setTimeout(function(){
        resizeCanvas();
        mouse.start(canvas, true);
        window.addEventListener("resize", resizeCanvas);
        requestAnimationFrame(update);
    },0);
})();
&#13;
&#13;
&#13;

演示中使用的图像

enter image description here enter image description here enter image description here

答案 1 :(得分:0)

我会尝试使用Box2D。我非常确定一旦你启动并运行引擎,你可以将对象链接在一起形成一个&#34;链&#34;喜欢集。请参阅链接中的演示。一些附属的身体部分是&#34;链接&#34;已经在一起了。