Created
July 24, 2012 01:17
-
-
Save anonymous/3167310 to your computer and use it in GitHub Desktop.
Slammer Fall Codea Example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# Emitter | |
Emitter = class() | |
function Emitter:init(args) | |
self.particleMesh = mesh() | |
self.particleMesh.texture = args.tex | |
self.minLife = args.minLife or 1 | |
self.maxLife = args.maxLife or 1 | |
self.spread = args.spread or 360 | |
self.angle = args.angle or 0 | |
self.minSpeed = args.minSpeed or 10 | |
self.maxSpeed = args.maxSpeed or 50 | |
self.minSize = args.minSize or 10 | |
self.maxSize = args.maxSize or 15 | |
self.growth = args.growth or 1 | |
self.startColor = args.startColor or color(255, 255, 255, 255) | |
self.endColor = args.endColor or color(0, 0, 0, 0) | |
self.streak = args.streak or false | |
self.streakMult = args.streakMult or 2 | |
self.accel = args.accel or vec2(0,0) | |
self.rAccel = args.rAccel or vec2(0,0) | |
self.particles = {} | |
self.pCount = 0 | |
for i = 1,100 do | |
table.insert(self.particles, Particle()) | |
end | |
end | |
function Emitter:emit(pos,count) | |
for i = 1,#self.particles do | |
local p = self.particles[i] | |
if p.dead then | |
self.pCount = math.max(i, self.pCount) | |
p.dead = false | |
p.pos = pos | |
p.life = math.random(self.minLife, self.maxLife) | |
p.size = math.random(self.minSize, self.maxSize) | |
p.maxLife = p.life | |
p.vel = vec2(0, math.random(self.minSpeed, self.maxSpeed)) | |
p.vel = p.vel:rotate(math.rad(self.angle + math.random(-self.spread/2, self.spread/2))) | |
count = count - 1 | |
if count == 0 then return end | |
end | |
end | |
end | |
function Emitter:draw() | |
-- update | |
self.particleMesh:clear() | |
for i = 1,self.pCount do | |
local p = self.particles[i] | |
if not p.dead then | |
p.prevPos = p.pos | |
p.pos = p.pos + p.vel * DeltaTime | |
p.vel = p.vel + (self.accel + vec2(math.random(-self.rAccel.x, self.rAccel.x), math.random(-self.rAccel.y, self.rAccel.y))) * DeltaTime | |
p.life = math.max(0, p.life - DeltaTime) | |
p.size = p.size + DeltaTime * self.growth | |
if p.life == 0 then p.dead = true end | |
local interp = p.life / p.maxLife | |
p.col.r = interp * self.startColor.r + (1-interp) * self.endColor.r | |
p.col.g = interp * self.startColor.g + (1-interp) * self.endColor.g | |
p.col.b = interp * self.startColor.b + (1-interp) * self.endColor.b | |
p.col.a = interp * self.startColor.a + (1-interp) * self.endColor.a | |
local ind = self.particleMesh:addRect(p.pos.x, p.pos.y, p.size, p.size) | |
self.particleMesh:setRectColor(ind, p.col) | |
if self.streak then | |
local dir = (p.pos - p.prevPos) | |
local len = dir:len() | |
local pos = (p.pos + p.prevPos) * 0.5 | |
local ang = math.atan2(dir.y, dir.x) | |
self.particleMesh:setRect(ind, pos.x, pos.y, p.size * self.streakMult, p.size, ang) | |
end | |
end | |
end | |
self.particleMesh:draw() | |
end | |
Particle = class() | |
function Particle:init() | |
self.pos = vec2(0,0) | |
self.prevPos = vec2(0,0) | |
self.vel = vec2(0,0) | |
self.life = 0 | |
self.maxLife = 0 | |
self.dead = true | |
self.col = color(0, 0, 0, 255) | |
self.size = 0 | |
end | |
--# SlammerPod | |
SlammerPod = class() | |
function SlammerPod:init(x,y) | |
self.name = "pod" | |
self.health = 1 | |
self.chassis = createBox(x, y, 60, 80) | |
self.chassis.gravityScale = 1 | |
self.chassis.density = 3 | |
self.chassis.fixedRotation = true | |
self.chassis.info = self | |
self.rock = createCircle(self.chassis.x,self.chassis.y - 150, 35) | |
self.rock.density = 0.75 | |
self.rock.info = self | |
owners[self.rock] = self | |
self.rock.angularDamping = 0.1 | |
self.rope = physics.joint(ROPE, self.chassis, self.rock, self.chassis.position, self.rock.position, 150) | |
debugDraw:addJoint(self.rope) | |
self.links = {} | |
local lc = 10 | |
local len = (self.chassis.y - self.rock.y) / lc | |
local p = self.chassis.position - vec2(0, len/2) | |
local prev = self.chassis | |
for i = 1,lc do | |
local link = createBox(p.x, p.y, 5, len) | |
link.mask = {10} | |
local j = physics.joint(REVOLUTE, prev, link, p + vec2(0, len/2)) | |
prev = link | |
debugDraw:addJoint(j) | |
table.insert(self.links, link) | |
p.y = p.y - len | |
end | |
local j = physics.joint(REVOLUTE, prev, self.rock, self.rock.position) | |
debugDraw:addJoint(j) | |
self.stick = VirtualStick({ | |
moved = function(v) self:stickMoved(v) end, | |
released = function(v) self:stickReleased(v) end, | |
radius = 80, | |
deadZoneRadius = 10, | |
}) | |
self.stick:activate() | |
self.dir = vec2(0,0) | |
self.anim = 0.0 | |
--sprite("Cargo Bot:Smoke Particle") | |
self.sparks = Emitter({tex = "Tyrian Remastered:Bullet Fire A", | |
minSize = 5, | |
maxSize = 5, | |
minSpeed = 100, | |
maxSpeed = 150, | |
accel = vec2(0,-1000), | |
rAccel = vec2(1500,1500), | |
streak = true, | |
streakMult = 3}) | |
self.smoke = Emitter({tex = "Cargo Bot:Smoke Particle", | |
minSize = 5, | |
maxSize = 25, | |
minSpeed = 0, | |
maxSpeed = 0, | |
accel = vec2(0,1000), | |
rAccel = vec2(1500,1500), | |
}) | |
end | |
function SlammerPod:stickMoved(v) | |
self.dir = v | |
end | |
function SlammerPod:stickReleased(v) | |
self.dir = vec2(0,0) | |
end | |
function SlammerPod:draw() | |
self.anim = self.anim + DeltaTime | |
local gain = 250 * self.chassis.mass | |
local damp = 0.75 | |
local vel = self.chassis.linearVelocity | |
self.chassis:applyForce(gain * self.dir - damp * vel) | |
self.chassis.linearDamping = vel:len() / 50 | |
pushMatrix() | |
translate(self.chassis.x, self.chassis.y) | |
rotate(self.chassis.angle) | |
sprite("Tyrian Remastered:Part H", 0, 0, 60) | |
pushMatrix() | |
rotate(self.anim * 100) | |
sprite("Tyrian Remastered:Orb 2", 0,0,30) | |
popMatrix() | |
popMatrix() | |
pushMatrix() | |
translate(self.rock.x, self.rock.y) | |
rotate(self.rock.angle) | |
sprite("Tyrian Remastered:Mine Spiked Huge", 0, 0, self.rock.radius * 2.5) | |
popMatrix() | |
for k,v in ipairs(self.links) do | |
pushMatrix() | |
translate(v.x,v.y) | |
rotate(v.angle) | |
sprite("Tyrian Remastered:Ring", 0,0,15,25) | |
popMatrix() | |
end | |
self.sparks:draw() | |
--self.()stick:draw() | |
end | |
function SlammerPod:hit(other, contact, first) | |
if contact.state == BEGAN then | |
local imp = math.max(0, (contact.normalImpulse-25) / 25) | |
if imp > 0.0 then | |
self.sparks:emit(contact.position, 10) | |
sound(SOUND_HIT, 47051, imp) | |
if other and other.damage then other:damage(imp) end | |
end | |
end | |
end | |
--# Worm | |
Worm = class() | |
function Worm:init(x,y,t) | |
self.name = "worm" | |
-- you can accept and set parameters here | |
self.body = createCircle(x,y,35) | |
self.body.info = self | |
self.body.gravityScale = 0 | |
self.target = t | |
self.dir = vec2(0,0) | |
self.health = 1 | |
self.power = 1 | |
self.stun = 0 | |
self.base = physics.body(CIRCLE, 35) | |
self.base.fixedRotation = true | |
self.base.gravityScale = 0 | |
self.base.position = self.body.position | |
self.base.mask = {15} | |
self.servo = physics.joint(REVOLUTE, self.base, self.body, self.base.position) | |
self.servo.enableMotor = true | |
self.servo.maxMotorTorque = 1000 | |
self.motorSpeed = 0 | |
end | |
function Worm:draw() | |
self.dir = (self.target.chassis.position - self.body.position) | |
self.dir = self.dir:normalize() | |
-- Codea does not automatically call this method | |
local gain = 29.400 * self.body.mass * self.power | |
local damp = 0.75 * self.power | |
local vel = self.body.linearVelocity | |
self.body:applyForce(gain * self.dir - damp * vel) | |
self.body.linearDamping = vel:len() / 100 * self.power | |
self.body.gravityScale = 1-self.power | |
local tAng = math.atan2(self.dir.y, self.dir.x) + math.pi/2 | |
self.servo.motorSpeed = (tAng - math.rad(self.servo.jointAngle)) * 100 | |
--if self.power == 0 | |
pushMatrix() | |
translate(self.body.x, self.body.y) | |
rotate(self.body.angle) | |
sprite("Tyrian Remastered:Organic Claw", 0,-14,60) | |
sprite("Tyrian Remastered:Part N") | |
popMatrix() | |
fill(120, 30, 30, 255) | |
rect(self.body.x,self.body.y+50,50,10) | |
fill(255, 0, 0, 255) | |
rect(self.body.x,self.body.y+50,50*self.health,10) | |
end | |
function Worm:damage(dmg) | |
self.health = math.max(0, self.health - dmg) | |
if self.health == 0 then | |
self.power = 0 | |
self.servo.enableMotor = false | |
sound(SOUND_RANDOM, 41402, 1) | |
end | |
end | |
WormPart = class() | |
function WormPart:init(x,y,r,worm,parent) | |
self.body = physics.body(CIRCLE, r) | |
self.body.x = x | |
self.body.y = y | |
self.worm = worm | |
self.health = 1 | |
end | |
--# Main | |
supportedOrientations(LANDSCAPE_ANY) | |
--displayMode(FULLSCREEN) | |
physics.resume() | |
-- Use this function to perform your initial setup | |
function setup() | |
debugDraw = PhysicsDebugDraw() | |
-- test classes | |
-- to add your own, make sure to define setup() and cleanup() | |
-- in addition to draw() and touched() | |
tests = {Test1()} | |
iparameter("test_number", 1, #tests) | |
iparameter("use_accelerometer", 0, 1) | |
currentTestIndex = 1 | |
currentTest = nil | |
setTest(currentTestIndex) | |
defaultGravity = physics.gravity() | |
end | |
function setTest(t) | |
if currentTest then | |
if currentTest.cleanup then | |
currentTest:cleanup() | |
end | |
cleanup() | |
end | |
currentTestIndex = t | |
currentTest = tests[t] | |
currentTest:setup() | |
end | |
function nextTest() | |
local t = currentTestIndex + 1 | |
if t > #tests then | |
t = 1 | |
end | |
setTest(t) | |
end | |
function createCircle(x,y,r) | |
local circle = physics.body(CIRCLE, r) | |
-- enable smooth motion | |
circle.interpolate = true | |
circle.x = x | |
circle.y = y | |
circle.restitution = 0.25 | |
debugDraw:addBody(circle) | |
return circle | |
end | |
function createBox(x,y,w,h) | |
-- polygons are defined by a series of points in counter-clockwise order | |
local box = physics.body(POLYGON, vec2(-w/2,h/2), vec2(-w/2,-h/2), vec2(w/2,-h/2), vec2(w/2,h/2)) | |
box.interpolate = true | |
box.x = x | |
box.y = y | |
box.restitutions = 0.25 | |
debugDraw:addBody(box) | |
return box | |
end | |
function createGround() | |
local ground = physics.body(POLYGON, vec2(0,20), vec2(0,0), vec2(WIDTH,0), vec2(WIDTH,20)) | |
ground.type = STATIC | |
debugDraw:addBody(ground) | |
return ground | |
end | |
function cleanup() | |
debugDraw:clear() | |
end | |
-- This function gets called once every frame | |
function draw() | |
-- This sets the background color to black | |
background(0, 0, 0) | |
if test_number ~= currentTestIndex then | |
setTest(test_number) | |
end | |
currentTest:draw() | |
--debugDraw:draw() | |
local str = string.format("Test %d - %s", currentTestIndex, currentTest.title) | |
text(str, WIDTH/2, HEIGHT - 20) | |
if use_accelerometer == 1 then | |
physics.gravity(Gravity) | |
else | |
--physics.gravity(defaultGravity) | |
end | |
end | |
function touched(touch) | |
if debugDraw:touched(touch) == false then | |
currentTest:touched(touch) | |
end | |
end | |
function collide(contact) | |
--debugDraw:collide(contact) | |
if currentTest.collide then | |
currentTest:collide(contact) | |
end | |
end | |
--# Test1 | |
Test1 = class() | |
function Test1:init() | |
self.title = "SlammerFall" | |
end | |
function Test1:setup() | |
POD = 0 | |
WORM = 1 | |
owners = {} | |
self.levelWidth = 1024*4 | |
self.levelHeight = 768*2 | |
self.stars = mesh() | |
self.stars.texture = "SpaceCute:Star" | |
self.stars2 = mesh() | |
self.stars2.texture = "SpaceCute:Star" | |
for i = 1,200 do | |
local size = math.random(5,15) | |
self.stars:addRect(math.random(0,self.levelWidth), math.random(0,self.levelHeight), size,size) | |
self.stars2:addRect(math.random(0,self.levelWidth*2), math.random(0,self.levelHeight*2), size,size) | |
end | |
self.pod = SlammerPod(self.levelWidth/2, HEIGHT/2) | |
self.worm1 = Worm(self.levelWidth, HEIGHT, self.pod) | |
self.worm1.name = "worm1" | |
self.worm2 = Worm(self.levelWidth/2, HEIGHT, self.pod) | |
self.worm2.name = "worm2" | |
self.worm3 = Worm(9, HEIGHT, self.pod) | |
self.worm3.name = "worm3" | |
self.camPos = self.pod.chassis.position | |
self.landMesh = mesh() | |
local points = {vec2(0,0)} | |
for x = 0,self.levelWidth,50 do | |
table.insert(points, vec2(x,50 + math.random(0, 25))) | |
end | |
table.insert(points, vec2(self.levelWidth, 0)) | |
self.landMesh.vertices = triangulate(points) | |
self.landMesh:setColors(128, 128, 128) | |
self.land = physics.body(CHAIN, true, unpack(points)) | |
self.land.type = STATIC | |
self.land.restitution = 0.3 | |
self.leftBarrier = physics.body(EDGE, vec2(0,0), vec2(0,self.levelHeight)) | |
self.rightBarrier = physics.body(EDGE, vec2(self.levelWidth,0), vec2(self.levelWidth,self.levelHeight)) | |
self.topBarrier = physics.body(EDGE, vec2(0,self.levelHeight), vec2(self.levelWidth,self.levelHeight)) | |
end | |
function Test1:draw() | |
-- Codea does not automatically call this method | |
pushMatrix() | |
local interp = 0.95 | |
self.camPos = self.camPos * interp + self.pod.chassis.position * (1-interp) | |
self.camPos.x = math.max(self.camPos.x, WIDTH/2) | |
self.camPos.x = math.min(self.camPos.x, self.levelWidth - WIDTH/2) | |
self.camPos.y = math.max(self.camPos.y, HEIGHT/2) | |
self.camPos.y = math.min(self.camPos.y, self.levelHeight - HEIGHT/2) | |
pushMatrix() | |
translate(-self.camPos.x/2 + WIDTH/2, -self.camPos.y/2 + HEIGHT/2) | |
self.stars2:draw() | |
popMatrix() | |
translate(-self.camPos.x + WIDTH/2, -self.camPos.y + HEIGHT/2) | |
self.stars:draw() | |
self.landMesh:draw() | |
self.pod:draw() | |
self.worm1:draw() | |
self.worm2:draw() | |
self.worm3:draw() | |
popMatrix() | |
end | |
function Test1:collide(contact) | |
if contact.state ~= BEGAN then return end | |
local infoA = contact.bodyA.info | |
local infoB = contact.bodyB.info | |
if infoA and infoA.hit then | |
infoA:hit(infoB, contact, true) | |
end | |
if infoB and infoB.hit then | |
infoB:hit(infoA, contact, false) | |
end | |
end | |
--# PhysicsDebugDraw | |
PhysicsDebugDraw = class() | |
function PhysicsDebugDraw:init() | |
self.bodies = {} | |
self.joints = {} | |
self.touchMap = {} | |
self.contacts = {} | |
end | |
function PhysicsDebugDraw:addBody(body) | |
table.insert(self.bodies,body) | |
end | |
function PhysicsDebugDraw:addJoint(joint) | |
table.insert(self.joints,joint) | |
end | |
function PhysicsDebugDraw:clear() | |
-- deactivate all bodies | |
for i,body in ipairs(self.bodies) do | |
body:destroy() | |
end | |
for i,joint in ipairs(self.joints) do | |
joint:destroy() | |
end | |
self.bodies = {} | |
self.joints = {} | |
self.contacts = {} | |
self.touchMap = {} | |
end | |
function PhysicsDebugDraw:draw() | |
pushStyle() | |
smooth() | |
strokeWidth(5) | |
stroke(128,0,128) | |
local gain = 2.0 | |
local damp = 0.5 | |
for k,v in pairs(self.touchMap) do | |
local worldAnchor = v.body:getWorldPoint(v.anchor) | |
local touchPoint = v.tp | |
local diff = touchPoint - worldAnchor | |
local vel = v.body:getLinearVelocityFromWorldPoint(worldAnchor) | |
v.body:applyForce( (1/1) * diff * gain - vel * damp, worldAnchor) | |
line(touchPoint.x, touchPoint.y, worldAnchor.x, worldAnchor.y) | |
end | |
stroke(0,255,0,255) | |
strokeWidth(5) | |
for k,joint in pairs(self.joints) do | |
local a = joint.anchorA | |
local b = joint.anchorB | |
line(a.x,a.y,b.x,b.y) | |
end | |
stroke(255,255,255,255) | |
noFill() | |
for i,body in ipairs(self.bodies) do | |
pushMatrix() | |
translate(body.x, body.y) | |
rotate(body.angle) | |
if body.type == STATIC then | |
stroke(255,255,255,255) | |
elseif body.type == DYNAMIC then | |
stroke(150,255,150,255) | |
elseif body.type == KINEMATIC then | |
stroke(150,150,255,255) | |
end | |
if body.shapeType == POLYGON then | |
strokeWidth(5.0) | |
local points = body.points | |
for j = 1,#points do | |
a = points[j] | |
b = points[(j % #points)+1] | |
line(a.x, a.y, b.x, b.y) | |
end | |
elseif body.shapeType == CHAIN or body.shapeType == EDGE then | |
strokeWidth(5.0) | |
local points = body.points | |
for j = 1,#points-1 do | |
a = points[j] | |
b = points[j+1] | |
line(a.x, a.y, b.x, b.y) | |
end | |
elseif body.shapeType == CIRCLE then | |
strokeWidth(5.0) | |
line(0,0,body.radius-3,0) | |
strokeWidth(2.5) | |
ellipse(0,0,body.radius*2) | |
end | |
popMatrix() | |
end | |
stroke(255, 0, 0, 255) | |
fill(255, 0, 0, 255) | |
for k,v in pairs(self.contacts) do | |
for m,n in ipairs(v.points) do | |
ellipse(n.x, n.y, 10, 10) | |
end | |
end | |
popStyle() | |
end | |
function PhysicsDebugDraw:touched(touch) | |
local touchPoint = vec2(touch.x, touch.y) | |
if touch.state == BEGAN then | |
for i,body in ipairs(self.bodies) do | |
if body.type == DYNAMIC and body:testPoint(touchPoint) then | |
self.touchMap[touch.id] = {tp = touchPoint, body = body, anchor = body:getLocalPoint(touchPoint)} | |
return true | |
end | |
end | |
elseif touch.state == MOVING and self.touchMap[touch.id] then | |
self.touchMap[touch.id].tp = touchPoint | |
return true | |
elseif touch.state == ENDED and self.touchMap[touch.id] then | |
self.touchMap[touch.id] = nil | |
return true; | |
end | |
return false | |
end | |
function PhysicsDebugDraw:collide(contact) | |
if contact.state == BEGAN then | |
self.contacts[contact.id] = contact | |
elseif contact.state == MOVING then | |
self.contacts[contact.id] = contact | |
elseif contact.state == ENDED then | |
self.contacts[contact.id] = nil | |
end | |
end | |
--# Controller | |
-- Base class for controllers | |
-- | |
-- Controllers translate touch events into callbacks to functions | |
-- that do something in the app. (Model/View/Controller style). | |
-- | |
-- Controllers can draw a representation of their current state on | |
-- the screen, but you can choose not to. | |
-- | |
-- A controller can be installed as the global handler for touch | |
-- events by calling its activate() method | |
Controller = class() | |
function Controller:activate() | |
touched = function(t) | |
self:touched(t) | |
end | |
end | |
function Controller:draw() | |
-- nothing | |
end | |
-- Utility functions | |
function touchPos(t) | |
return vec2(t.x, t.y) | |
end | |
function clamp(x, min, max) | |
return math.max(min, math.min(max, x)) | |
end | |
function clampAbs(x, maxAbs) | |
return clamp(x, -maxAbs, maxAbs) | |
end | |
function clampLen(vec, maxLen) | |
return vec:normalize() * math.min(vec:len(), maxLen) | |
end | |
-- projects v onto the direction represented by the given unit vector | |
function project(v, unit) | |
return v:dot(unit) | |
end | |
function sign(x) | |
if x == 0 then | |
return 0 | |
elseif x < 0 then | |
return -1 | |
elseif x > 0 then | |
return 1 | |
else | |
return x -- x is NaN | |
end | |
end | |
function doNothing() | |
end | |
--# VirtualStick | |
-- A virtual analogue joystick with a dead-zone at the center, | |
-- which activates wherever the user touches their finger | |
-- | |
-- Arguments: | |
-- radius - radius of the stick (default = 100) | |
-- deadZoneRadius - radius of the stick's dead zone (default = 25) | |
-- moved(v) - Called when the stick is moved | |
-- v : vec2 - in the range vec2(-1,-1) and vec2(1,1) | |
-- pressed() - Called when the user starts using the stick (optional) | |
-- released() - Called when the user releases the stick (optional) | |
VirtualStick = class(Controller) | |
function VirtualStick:init(args) | |
self.radius = args.radius or 100 | |
self.deadZoneRadius = args.deadZoneRadius or 25 | |
self.releasedCallback = args.released or doNothing | |
self.steerCallback = args.moved or doNothing | |
self.pressedCallback = args.pressed or doNothing | |
end | |
function VirtualStick:touched(t) | |
local pos = touchPos(t) | |
if t.state == BEGAN and self.touchId == nil then | |
self.touchId = t.id | |
self.touchStart = pos | |
self.stickOffset = vec2(0, 0) | |
self.pressedCallback() | |
elseif t.id == self.touchId then | |
if t.state == MOVING then | |
self.stickOffset = clampLen(pos - self.touchStart, self.radius) | |
self.steerCallback(self:vector()) | |
elseif t.state == ENDED then | |
self:reset() | |
self.releasedCallback() | |
end | |
end | |
end | |
function VirtualStick:vector() | |
local stickRange = self.radius - self.deadZoneRadius | |
local stickAmount = math.max(self.stickOffset:len() - self.deadZoneRadius, 0) | |
local stickDirection = self.stickOffset:normalize() | |
return stickDirection * (stickAmount/stickRange) | |
end | |
function VirtualStick:reset() | |
self.touchId = nil | |
self.touchStart = nil | |
self.stickOffset = nil | |
end | |
function VirtualStick:draw() | |
if self.touchId ~= nil then | |
pushStyle() | |
ellipseMode(RADIUS) | |
strokeWidth(1) | |
stroke(255, 255, 255, 255) | |
noFill() | |
pushMatrix() | |
translate(self.touchStart.x, self.touchStart.y) | |
ellipse(0, 0, self.radius, self.radius) | |
ellipse(0, 0, self.deadZoneRadius, self.deadZoneRadius) | |
translate(self.stickOffset.x, self.stickOffset.y) | |
ellipse(0, 0, 25, 25) | |
popMatrix() | |
popStyle() | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment