DEV Community

Cover image for HWYDT: Swinging from Vines
JoeStrout
JoeStrout

Posted on

HWYDT: Swinging from Vines

The Venerable History of Vine-Swinging

Swinging on vines has been a favorite game mechanic since 1982, when both Jungle Hunt (a thinly disguised Tarzan arcade game) and Pitfall! (on of the Atari 2600's best-selling action games) hit the market.

Animated GIF of Pitfall!

Since then, swinging from vines, ropes, webs, grappling hooks, extensible bionic arms, etc. has been a recurring staple (including in such modern hits as Spider-Pig!).

But how can you actually do this in your own games? Let's dig in and find out!

The Big Picture

We're going to implement a vine-swinging demo in Mini Micro. Our vines will be composed of five or so straight segments, connected like links in a chain. These will oscillate back and forth, using some simple trigonometry (don't worry, I said simple and I meant it!) to update their positions and rotations. We need a sprite image for each vine segment; this should be a thick line with rounded ends (so they join neatly together), and for decoration, I've added some leaves:

VineSegment.png

Go ahead and save this image as VineSegment.png, and fire up Mini Micro (download it here if you don't have it already). Notice that the image is mostly empty on the left; the "pivot point" (around which the sprite rotates) in Mini Micro is always in the center of the image, so we've simply offset our drawing so that the center of the image is at one end of the visible segment.

Our hero (Kip) will be, at any given moment, either attached to some vine segment, or flying ballistically through the air. When attached, we just need to update his position along with the vine segment he's clinging to. When flying, then Isaac Newton is in charge, and fortunately his equations are even simpler.

Preliminaries

In Mini Micro, enter edit to start a new program, and then paste in these preliminary steps to clear the screen, get a handy reference to the sprite display, and load some sounds we'll need later.

import "mathUtil"

clear
spriteDisp = display(4)
jumpSound = file.loadSound("/sys/sounds/pickup.wav")
catchSound = file.loadSound("/sys/sounds/swoosh.wav")
Enter fullscreen mode Exit fullscreen mode

Then, we're going to need a bit of math to convert between local and world coordinates. By "local" coordinates, I mean coordinates relative to a sprite — for example, the pixel coordinates of some point in its image; those are local to the sprite. "World" coordinates are basically positions on the screen, except that we're going to be scrolling the view our hero progresses, so it's more accurate to say they are positions relative to the sprite display. We need to convert back and forth: converting from local to world in order to position each vine segment relative to the previous one, or Kip relative to the segment he's holding; and converting from world to local to see if Kip is close enough to grab a vine when flying through the air.

This is where the trigonometry comes in. Honestly, I don't memorize or derive these equations; I just google 'em when I need them. You can just copy and paste this into your program:

// Set the x and y values of the given map to
// the world position corresponding to the
// given XY local to this sprite.  In other words,
// assume targetMap is attached to this sprite
// at local position localX, localY; update its
// x and y accordingly.
Sprite.setXYtoLocal = function(targetMap, localX, localY)
    radians = self.rotation * pi/180
    cosAng = cos(radians)
    sinAng = sin(radians)
    targetMap.x = self.x + localX * cosAng - localY * sinAng
    targetMap.y = self.y + localX * sinAng + localY * cosAng    
end function

// Get the local [x,y] position of some world object
// relative to this sprite.
Sprite.getLocal = function(worldXY)
    radians = self.rotation * pi/180
    dx = worldXY.x - self.x
    dy = worldXY.y - self.y
    cosAng = cos(radians)
    sinAng = sin(radians)
    return [dx * cosAng + dy * sinAng, -dx * sinAng + dy * cosAng]  
end function
Enter fullscreen mode Exit fullscreen mode

Save your program as swing.ms (or whatever you like), and run it just to be sure you haven't made a syntax error. If it seems to do nothing, you're doing great so far!

Swingy vines

Now let's add the code that actually makes a vine. We'll have a Sprite subclass called Segment, just to give them all a common image and length; and then we'll have another class called Vine, which encapsulates a list of segments and knows how to update them.

Segment = new Sprite
Segment.image = file.loadImage("VineSegment.png")
Segment.length = 64

Vine = {}
Vine.angRange = 55  // how far to swing from vertical (degrees)
Vine.period = 2  // how long a full back-and-forth takes (seconds)
Vine.Instances = []

Vine.Make = function(x=480, y=640, qtySegments=5)
    vine = new self
    vine.segments = []
    for i in range(0, qtySegments-1)
        seg = new Segment
        seg.x = x
        seg.y = y - seg.length * i
        seg.rotation = -90
        spriteDisp.sprites.push seg
        vine.segments.push seg
    end for
    Vine.Instances.push vine
    return vine
end function
Vine.update = function(time)
    t = 2*pi * time/self.period
    for i in self.segments.indexes
        seg = self.segments[i]
        ang = -90 + self.angRange * sin(t - i*0.2)
        seg.rotation = ang
        if i < self.segments.len-1 then
            seg.setXYtoLocal self.segments[i+1], seg.length, 0
        end if
    end for
end function
Vine.UpdateAll = function(time)
    for vine in Vine.Instances
        vine.update time
    end for
end function
Enter fullscreen mode Exit fullscreen mode

The key bit is of course the Vine.update function, which iterates over its segments, setting the angle of each, and then the position of the next using that setXYtoLocal function we prepared before. The angle is set according to the time (divided by self.period, which is how we make vines swing faster or slower), and subtracts a little factor of the segment number, i * 0.2. This makes the end of the vine lag a bit relative to the top of the vine, making it look like a rope/chain rather than a rigid bar.

To test this code, let's add a simple main program that creates one vine, and updates it:

Vine.Make
while true
    yield
    Vine.UpdateAll time
end while
Enter fullscreen mode Exit fullscreen mode

Now run, and it should look like this:

Animated GIF of swinging vine

Challenge: Add a second vine, at a different position. Double challenge: give it a different period, so they're not swinging in perfect sync!

Our Hero, Kip

Now we need a player character to jump from vine to vine. We'll use our hero Kip (fresh from the Caves of Lava), since his sprites are included with Mini Micro. Delete the mini-main-program you added above, and paste in the code below.

kip = new Sprite
kip.image = file.loadImage("/sys/pics/KP/KP-jump.png")
kip.grabbed = null  // Segment he's hanging onto
kip.grabPos = [0,0] // grab position local to that segment
kip.vx = 0; kip.vy = 0  // velocity, in pixels/sec

kip.update = function(dt)
    if self.grabbed then
        // Stick to the vine, updating our velocity as we go
        lastPos = [self.x, self.y]
        self.grabbed.setXYtoLocal self, self.grabPos[0], self.grabPos[1]
        self.vx = (self.x - lastPos[0]) / dt
        self.vy = (self.y - lastPos[1]) / dt
    else
        // free flying!
        self.vy -= 1000*dt  // gravity
        self.x += self.vx * dt
        self.y += self.vy * dt
        // Try to catch any vine except our last one.
        for vine in Vine.Instances
            if vine == self.lastVine then continue
            if self.tryCatch(vine) then break
        end for
    end if
end function

kip.jump = function(extraVx=0, extraVy=100)
    if not self.grabbed then return
    jumpSound.play
    self.grabbed = null
    self.vy += 100  
end function

kip.tryCatch = function(vine)
    for seg in vine.segments
        localPos = seg.getLocal(self)
        if (0 <= localPos[0] < seg.length) and
          (-12 <= localPos[1] < 12) then
            // Valid catch!
            self.grabbed = seg
            self.grabPos = localPos
            self.lastVine = vine
            catchSound.play
            return true
        end if
    end for
    return false
end function
Enter fullscreen mode Exit fullscreen mode

As you can see, Kip's update method has two modes: it does one thing when he has grabbed a vine, and something else when he is flying through space. Both cases are pretty simple. An important trick in the first case is updating kip's velocity (vx and vy) based on how the vine is causing him to move. That matters because when you jump, you want to continue with (more or less) that same velocity.

In the free-flying case, we just apply some gravity to our vertical velocity (vy), and then update our position according to current velocity. The only tricky bit here is checking to see when we're close enough to another vine to grab it — so I extracted that into its own method, kip.tryCatch. This iterates over the segments of the given vine, and sees what our position would be local to that segment. If it's close enough, then we do the catch. (Who says MiniScript doesn't have try/catch?!)

Camera, Final Setup, and Main Program

Our demo is almost done, so let's press on with the final bit: a function to scroll the display so that the "camera" stays centered on the current vine; some code to create a bunch of vines, increasingly far apart; and the main program.


updateCamera = function(dt)
    // try to center the active vine on the screen
    targetX = kip.lastVine.segments[0].x - 480
    spriteDisp.scrollX = mathUtil.moveTowards(
      spriteDisp.scrollX, targetX, 200 * dt)  
end function

// create the vines
x = 300; dx = 400
while x < 4000
    Vine.Make(x).period = 1.8 + rnd*0.4
    dx += 35
    x += dx
end while

// place Kip on the first vine
kip.lastVine = Vine.Instances[0]
kip.grabbed = kip.lastVine.segments[-2]
kip.grabPos = [40, 0]
kip.update
spriteDisp.sprites.push kip

// main loop
lastTime = time
jumpWasDown = false
while kip.y > 0
    yield
    now = time
    dt = now - lastTime
    lastTime = now
    Vine.UpdateAll now
    kip.update dt
    jumpDown = key.pressed("space")
    if jumpDown and not jumpWasDown then kip.jump
    jumpWasDown = jumpDown
    updateCamera dt
end while
key.clear
text.row = 1
print "Game over!"
Enter fullscreen mode Exit fullscreen mode

And that's it! Run now and you should have a simple but playable game. How far can you make it before you fall?

Animated screencap of swing demo

Full Program

Here's the whole program in one big listing.
import "mathUtil"

clear
spriteDisp = display(4)
jumpSound = file.loadSound("/sys/sounds/pickup.wav")
catchSound = file.loadSound("/sys/sounds/swoosh.wav")

// Set the x and y values of the given map to
// the world position corresponding to the
// given XY local to this sprite.  In other words,
// assume targetMap is attached to this sprite
// at local position localX, localY; update its
// x and y accordingly.
Sprite.setXYtoLocal = function(targetMap, localX, localY)
    radians = self.rotation * pi/180
    cosAng = cos(radians)
    sinAng = sin(radians)
    targetMap.x = self.x + localX * cosAng - localY * sinAng
    targetMap.y = self.y + localX * sinAng + localY * cosAng    
end function

// Get the local [x,y] position of some world object
// relative to this sprite.
Sprite.getLocal = function(worldXY)
    radians = self.rotation * pi/180
    dx = worldXY.x - self.x
    dy = worldXY.y - self.y
    cosAng = cos(radians)
    sinAng = sin(radians)
    return [dx * cosAng + dy * sinAng, -dx * sinAng + dy * cosAng]  
end function

Segment = new Sprite
Segment.image = file.loadImage("VineSegment.png")
Segment.length = 64

Vine = {}
Vine.angRange = 55  // how far to swing from vertical (degrees)
Vine.period = 2  // how long a full back-and-forth takes (seconds)
Vine.Instances = []

Vine.Make = function(x=480, y=640, qtySegments=5)
    vine = new self
    vine.segments = []
    for i in range(0, qtySegments-1)
        seg = new Segment
        seg.x = x
        seg.y = y - seg.length * i
        seg.rotation = -90
        spriteDisp.sprites.push seg
        vine.segments.push seg
    end for
    Vine.Instances.push vine
    return vine
end function
Vine.update = function(time)
    t = 2*pi * time/self.period
    for i in self.segments.indexes
        seg = self.segments[i]
        ang = -90 + self.angRange * sin(t - i*0.2)
        seg.rotation = ang
        if i < self.segments.len-1 then
            seg.setXYtoLocal self.segments[i+1], seg.length, 0
        end if
    end for
end function
Vine.UpdateAll = function(time)
    for vine in Vine.Instances
        vine.update time
    end for
end function

kip = new Sprite
kip.image = file.loadImage("/sys/pics/KP/KP-jump.png")
kip.grabbed = null  // Segment he's hanging onto
kip.grabPos = [0,0] // grab position local to that segment
kip.vx = 0; kip.vy = 0  // velocity, in pixels/sec

kip.update = function(dt)
    if self.grabbed then
        // Stick to the vine, updating our velocity as we go
        lastPos = [self.x, self.y]
        self.grabbed.setXYtoLocal self, self.grabPos[0], self.grabPos[1]
        self.vx = (self.x - lastPos[0]) / dt
        self.vy = (self.y - lastPos[1]) / dt
    else
        // free flying!
        self.vy -= 1000*dt  // gravity
        self.x += self.vx * dt
        self.y += self.vy * dt
        // Try to catch any vine except our last one.
        for vine in Vine.Instances
            if vine == self.lastVine then continue
            if self.tryCatch(vine) then break
        end for
    end if
end function

kip.jump = function(extraVx=0, extraVy=100)
    if not self.grabbed then return
    jumpSound.play
    self.grabbed = null
    self.vy += 100  
end function

kip.tryCatch = function(vine)
    for seg in vine.segments
        localPos = seg.getLocal(self)
        if (0 <= localPos[0] < seg.length) and
          (-12 <= localPos[1] < 12) then
            // Valid catch!
            self.grabbed = seg
            self.grabPos = localPos
            self.lastVine = vine
            catchSound.play
            return true
        end if
    end for
    return false
end function

updateCamera = function(dt)
    // try to center the active vine on the screen
    targetX = kip.lastVine.segments[0].x - 480
    spriteDisp.scrollX = mathUtil.moveTowards(
      spriteDisp.scrollX, targetX, 200 * dt)  
end function

// create the vines
x = 300; dx = 400
while x < 4000
    Vine.Make(x).period = 1.8 + rnd*0.4
    dx += 35
    x += dx
end while

// place Kip on the first vine
kip.lastVine = Vine.Instances[0]
kip.grabbed = kip.lastVine.segments[-2]
kip.grabPos = [40, 0]
kip.update
spriteDisp.sprites.push kip

// main loop
lastTime = time
jumpWasDown = false
while kip.y > 0
    yield
    now = time
    dt = now - lastTime
    lastTime = now
    Vine.UpdateAll now
    kip.update dt
    jumpDown = key.pressed("space")
    if jumpDown and not jumpWasDown then kip.jump
    jumpWasDown = jumpDown
    updateCamera dt
end while
key.clear
text.row = 1
print "Game over!"
Enter fullscreen mode Exit fullscreen mode

Taking it Further

This simple demo could be quickly expanded into a more engaging game:

  • Keep a score, adding points for every successful jump, and/or for reaching new vines.
  • Add hazards (water, alligators, whatever) below to heighten the tension.
  • Add platforms/ground with traditional platformer mechanics (e.g. running and climbing).
  • Enable Kip to climb up and down the vine he's on (this makes it a lot easier!).
  • Improve the vine physics, allowing stretching, slinging, and other fun effects, much like Spider-Pig.

Can you think of other good uses for this sort of mechanic? How might you apply this in your own games? Share your thoughts in the comments below!

Top comments (1)

Collapse
 
treytomes profile image
Trey Tomes

Pitfall! is unique in that it's levels are procedurally generated. It'd be fun to see a Mini Micro game that pulls on that heritage.