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.
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:
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")
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
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
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
Now run, and it should look like this:
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
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!"
And that's it! Run now and you should have a simple but playable game. How far can you make it before you fall?
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!"
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)
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.