Как я могу заставить персонажа ходить по неровным стенам в 2D платформере?

11

Я хочу иметь игрового персонажа, который может «ходить» по органической поверхности под любым углом, в том числе вбок и вверх ногами. Благодаря «органическим» уровням со скошенными и изогнутыми элементами вместо прямых линий под углом 90 градусов.

В настоящее время я работаю в AS3 (умеренный любительский опыт) и использую Nape (в значительной степени новичок) для базовой физики, основанной на гравитации, к которой эта механика ходьбы будет очевидным исключением.

Есть ли процедурный способ сделать этот вид механики ходьбы, возможно, используя ограничения Nape? Или было бы лучше создать явные "дорожки" для ходьбы, следуя контурам поверхностей уровня, и использовать их для ограничения движения при ходьбе?

Эрик Н
источник
Чтобы уточнить: вы хотите, чтобы ваш персонаж мог «прилипать» к стенам и потолкам на вашем уровне?
Qqwy
Правильно.
Eric N

Ответы:

9

Вот мой полный опыт обучения, в результате чего я получил довольно функциональную версию движения, которое я хотел, используя все внутренние методы Nape. Весь этот код находится в моем классе Spider, извлекая некоторые свойства из его родителя, класса Level.

Большинство других классов и методов являются частью пакета Nape. Вот соответствующая часть моего списка импорта:

import flash.events.TimerEvent;
import flash.utils.Timer;

import nape.callbacks.CbEvent;
import nape.callbacks.CbType;
import nape.callbacks.InteractionCallback;
import nape.callbacks.InteractionListener;
import nape.callbacks.InteractionType;
import nape.callbacks.OptionType;
import nape.dynamics.Arbiter;
import nape.dynamics.ArbiterList;
import nape.geom.Geom;
import nape.geom.Vec2;

Во-первых, когда на сцену добавляется паук, я добавляю слушателей в мир затылка для столкновений. По мере продвижения в развитии мне нужно будет различать группы столкновений; на данный момент эти обратные вызовы будут технически выполняться, когда ЛЮБОЕ тело сталкивается с любым другим телом.

        var opType:OptionType = new OptionType([CbType.ANY_BODY]);
        mass = body.mass;
        // Listen for collision with level, before, during, and after.
        var landDetect:InteractionListener =  new InteractionListener(CbEvent.BEGIN, InteractionType.COLLISION, opType, opType, spiderLand)
        var moveDetect:InteractionListener =  new InteractionListener(CbEvent.ONGOING, InteractionType.COLLISION, opType, opType, spiderMove);
        var toDetect:InteractionListener =  new InteractionListener(CbEvent.END, InteractionType.COLLISION, opType, opType, takeOff);

        Level(this.parent).world.listeners.add(landDetect);
        Level(this.parent).world.listeners.add(moveDetect);
        Level(this.parent).world.listeners.add(toDetect);

        /*
            A reference to the spider's parent level's master timer, which also drives the nape world,
            runs a callback within the spider class every frame.
        */
        Level(this.parent).nTimer.addEventListener(TimerEvent.TIMER, tick);

Обратные вызовы изменяют свойство «состояния» паука, которое является набором логических значений, и записывают любые арбитры столкновения Nape для последующего использования в моей логике ходьбы. Они также устанавливают и сбрасывают toTimer, который позволяет пауку потерять контакт с поверхностью уровня на срок до 100 мс, прежде чем снова станет действовать мировая гравитация.

    protected function spiderLand(callBack:InteractionCallback):void {
        tArbiters = callBack.arbiters.copy();
        state.isGrounded = true;
        state.isMidair = false;
        body.gravMass = 0;
        toTimer.stop();
        toTimer.reset();
    }

    protected function spiderMove(callBack:InteractionCallback):void {
        tArbiters = callBack.arbiters.copy();
    }

    protected function takeOff(callBack:InteractionCallback):void {
        tArbiters.clear();
        toTimer.reset();
        toTimer.start();
    }

    protected function takeOffTimer(e:TimerEvent):void {
        state.isGrounded = false;
        state.isMidair = true;
        body.gravMass = mass;
        state.isMoving = false;
    }

Наконец, я вычисляю, какие силы применить к пауку, основываясь на его состоянии и его связи с геометрией уровня. Я в основном позволю комментариям говорить за себя.

    protected function tick(e:TimerEvent):void {
        if(state.isGrounded) {
            switch(tArbiters.length) {
                /*
                    If there are no arbiters (i.e. spider is in midair and toTimer hasn't expired),
                    aim the adhesion force at the nearest point on the level geometry.
                */
                case 0:
                    closestA = Vec2.get();
                    closestB = Vec2.get();
                    Geom.distanceBody(body, lvBody, closestA, closestB);
                    stickForce = closestA.sub(body.position, true);
                    break;
                // For one contact point, aim the adhesion force at that point.
                case 1:
                    stickForce = tArbiters.at(0).collisionArbiter.contacts.at(0).position.sub(body.position, true);
                    break;
                // For multiple contact points, add the vectors to find the average angle.
                default:
                    var taSum:Vec2 = tArbiters.at(0).collisionArbiter.contacts.at(0).position.sub(body.position, true);
                    tArbiters.copy().foreach(function(a:Arbiter):void {
                        if(taSum != a.collisionArbiter.contacts.at(0).position.sub(body.position, true))
                            taSum.addeq(a.collisionArbiter.contacts.at(0).position.sub(body.position, true));
                    });

                    stickForce=taSum.copy();
            }
            // Normalize stickForce's strength.
            stickForce.length = 1000;
            var curForce:Vec2 = new Vec2(stickForce.x, stickForce.y);

            // For graphical purposes, align the body (simulation-based rotation is disabled) with the adhesion force.
            body.rotation = stickForce.angle - Math.PI/2;

            body.applyImpulse(curForce);

            if(state.isMoving) {
                // Gives "movement force" a dummy value since (0,0) causes problems.
                mForce = new Vec2(10,10);
                mForce.length = 1000;

                // Dir is movement direction, a boolean. If true, the spider is moving left with respect to the surface; otherwise right.
                // Using the corrected "down" angle, move perpendicular to that angle
                if(dir) {
                    mForce.angle = correctAngle()+Math.PI/2;
                } else {
                    mForce.angle = correctAngle()-Math.PI/2;
                }
                // Flip the spider's graphic depending on direction.
                texture.scaleX = dir?-1:1;
                // Now apply the movement impulse and decrease speed if it goes over the max.
                body.applyImpulse(mForce);
                if(body.velocity.length > 1000) body.velocity.length = 1000;

            }
        }
    }

По-настоящему липкая часть, которую я обнаружил, заключалась в том, что угол движения должен был соответствовать реальному желаемому направлению движения в сценарии с несколькими точками контакта, когда паук достигает острого угла или сидит в глубокой долине. Тем более что, учитывая мои суммарные векторы силы адгезии, эта сила будет ТЯЖЕТЬ в направлении, в котором мы хотим двигаться, а не перпендикулярно ей, поэтому нам нужно противодействовать этому. Поэтому мне была нужна логика, чтобы выбрать одну из точек контакта, чтобы использовать ее в качестве основы для угла вектора движения.

Побочным эффектом «тяги» силы сцепления является небольшое колебание, когда паук достигает острого вогнутого угла / кривой, но это на самом деле довольно реалистично с точки зрения внешнего вида, поэтому, если это не вызовет проблем в будущем, я буду оставь как есть. Если мне нужно, я могу использовать вариацию этого метода для расчета силы сцепления.

    protected function correctAngle():Number {
        var angle:Number;
        if(tArbiters.length < 2) {
            // If there is only one (or zero) contact point(s), the "corrected" angle doesn't change from stickForce's angle.
            angle = stickForce.angle;
        } else {
            /*
                For more than one contact point, we want to run perpendicular to the "new" down, so we copy all the
                contact point angles into an array...
            */
            var angArr:Array = [];
            tArbiters.copy().foreach(function(a:Arbiter):void {
                var curAng:Number = a.collisionArbiter.contacts.at(0).position.sub(body.position, true).angle;
                if (curAng < 0) curAng += Math.PI*2;
                angArr.push(curAng);
            });
            /*
                ...then we iterate through all those contact points' angles with respect to the spider's COM to figure out
                which one is more clockwise or more counterclockwise, depending, with some restrictions...
                ...Whatever, the correct one.
            */
            angle = angArr[0];
            for(var i:int = 1; i<angArr.length; i++) {
                if(dir) {
                    if(Math.abs(angArr[i]-angle) < Math.PI)
                        angle = Math.max(angle, angArr[i]);
                    else
                        angle = Math.min(angle, angArr[i]);
                }
                else {
                    if(Math.abs(angArr[i]-angle) < Math.PI)
                        angle = Math.min(angle, angArr[i]);
                    else
                        angle = Math.max(angle, angArr[i]);
                }
            }
        }

        return angle;
    }

Эта логика в значительной степени «идеальна», поскольку до сих пор кажется, что она делает то, что я хочу. Тем не менее, существует давняя косметическая проблема, заключающаяся в том, что, если я попытаюсь выровнять изображение паука с силами сцепления или движения, я обнаружу, что паук в конечном итоге «наклоняется» в направлении движения, что было бы хорошо, если бы он был Двуногий спортивный спринтер, но он не так, и углы очень чувствительны к изменениям на местности, поэтому паук дрожит, когда он пересекает малейший выступ. Я могу использовать вариант решения Byte56, отобрав ближайший ландшафт и усреднив эти углы, чтобы сделать ориентацию паука более плавной и реалистичной.

Эрик Н
источник
1
Отлично, спасибо за размещение подробностей здесь для будущих посетителей.
MichaelHouse
8

Как насчет того, чтобы заставить любую "палку" поверхности, к которой прикоснулся персонаж, применить силу вдоль обратной нормали к поверхности? Сила сохраняется до тех пор, пока они находятся в контакте с поверхностью, и перекрывает гравитацию, пока она активна. Таким образом, прыжки с потолка будут иметь ожидаемый эффект от падения на пол.

Возможно, вы захотите реализовать некоторые другие функции, чтобы сделать это гладко и проще в реализации. Например, вместо того, что касается персонажа, используйте круг вокруг персонажа и суммируйте перевернутые нормали. Как показывает эта дрянная краска:

введите описание изображения здесь

(Представленное изображение паука является собственностью Byte56)

Синие линии - это обратные нормали к поверхности в этой точке. Зеленая линия - это суммарная сила, приложенная к пауку. Красный круг представляет диапазон, который паук ищет для нормалей.

Это позволило бы получить некоторую неровность на местности без того, чтобы паук «потерял свою хватку». Поэкспериментируйте с размером и формой круга, возможно, просто используйте полукруг, ориентированный пауком вниз, может быть, просто прямоугольник, который охватывает ноги.

Это решение позволяет вам держать физику включенной без необходимости иметь дело с конкретными путями, по которым персонаж может следовать. Он также использует информацию, которую довольно легко получить и интерпретировать (нормали). Наконец, это динамично. Легко объяснить даже изменение формы мира, поскольку вы можете легко получить нормали для любой геометрии, которую вы рисуете.

Помните, что когда лица не находятся в пределах досягаемости паука, возникает нормальная гравитация.

MichaelHouse
источник
Суммированные нормали, вероятно, решат проблемы, с которыми мое текущее решение сталкивается с острыми вогнутыми углами, но я не знаю, как вы получаете их в AS3.
Eric N
Извините, я тоже не знаком. Возможно, что-то, что вам нужно для поддержания себя при создании ландшафта.
MichaelHouse
2
Мне удалось реализовать эту идею, поскольку я могу обнаружить точки контакта столкновения Nape и усреднить их, если их несколько. Кажется, что нет необходимости перемещаться по плоским или выпуклым поверхностям, но это решило мою самую большую проблему: что делать, когда мой паук сталкивается с острым углом. Как уже упоминалось в моем новом ответе, я могу попробовать вариант этой идеи, чтобы помочь сориентировать изображение моего паука.
Eric N