Are there callbacks available on craft.rigidbody in Codea?

I’m learning about craft.physics, and I’m confused as to the best way to detect collisions between craft.rigidbodies in Codea. My understanding is that many of Craft’s features were modeled after Unity, and I saw in Unity there are “OnCollision” callbacks that programmers can use to detect when two rigid bodies collide but don’t see such callbacks listed in the Codea reference. I do see some examples posted of object collisions, but they seem to rely on calculating the actual positions between objects to determine if there was a collision. However, I thought that would have been the purpose of attaching a rigidbody to an entity so that you could use a callback to determine when the two rigidbodies collide instead of having to calculate each entities’ position. If not, why would one need to assign a “static” rigidbody to a wall entity, for example, if Codea doesn’t let the programmer know that a “dynamic” rigid body character just collided into that “static” rigidbody wall? Is there instead another reason to attach a static rigidbody to an entity in Codea?

I did see there is a rayCast function that can be used between rigidbodies— is that perhaps how we should detect collisions between our dynamic rigidbody character craft.models and static rigidbodies craft.model walls (e.g. by casting a ray out in front of our 3D characters to see if there is a wall in front of them— like sonar?)

I appreciate anyone’s further information on Codea’s 3D collision detection and why one would add a static rigidbody to a 3D entity.

Thanks!

@SugarRay Look in the documentation under physics/Contact Overview. The collide function is called anytime there’s a collision between physics object. In that function you can query what objects collided and other info.

@SugarRay Here’s an example.

function setup() 
    c1 = physics.body(CIRCLE,100)    -- create body for 1st circle
    c1.type=STATIC
    c1.x=WIDTH/2
    c1.y=300
    c1.name="c1 circle"
    
    c2 = physics.body(CIRCLE,50)    -- create body for 2nd circle
    c2.x=WIDTH/2
    c2.y=800
    c2.restitution=1
    c2.name="c2 circle"
    
    count=0
end

function draw()
    background(40,40,50)
    fill(255)
    ellipse(c1.x,c1.y,200,200)    -- draw the 1st circle
    ellipse(c2.x,c2.y,100,100)    -- draw the 2nd circle   
end

function collide(c)
    if c.state==BEGAN then
        output.clear()
        count=count+1
        print("collision "..count)
        print("c1",c1)
        print("c2",c2)
        print("bodyA",c.bodyA)
        print("bodyB",c.bodyB)
        print("radius of body A ",c.bodyA.radius)
        print("radius of body B ",c.bodyB.radius)
        print("x position of body A ",c.bodyA.x)
        print("y position of body A ",c.bodyA.y)
        print("bodyA ",c.bodyA.name)
        print("bodyB ",c.bodyB.name)
        -- see physics.contact and physics.body in the reference manual
        --    for other info that's available during a collision
    end    
end

So far, it looks like CODEA has two physical systems, one for 2D environments (using Box2D) and one for 3D environments (craft.physics) .
The above example of @dave1707 is a 2D physics.

I’m also studying craft collision detection, and in my opinion, there are two types of collisions in craft physical systems:

  • one is collision detection between objects in a scene, such as a character with a physical rigid attached to it, and a section of the wall is attached with a physical rigid, the collision between them will be automatically detected, resulting in the wall will stop the movement of the role;
  • The other is when the user clicks on the screen to interact with the entities in the scene. The screen plane coordinates need to be converted to the spatial coordinates in the scene, which need to be computed by raycast.

There are also two slightly different physical systems in craft:

  • one that uses the OBJ model;
  • the other is to use the voxels model (craft.volume) of the physical system.

The first allows precise space occupancy calculations by attaching a physical rigid, while the second requires manual coding with bounds to determine the approximate footprint.

Thanks, @binaryblues and @dave1707,

I saw the physics/Contact overview but was assuming that only applied to 2D Codea objects, and I was looking to detect collisions in 3D. I tried converting what dave1707 suggested in 3D craft but it didn’t seem to work (i.e. function collision(collide) never detected a collision between the static rigidbody wall and the dynamic rigidbody character I made).

My workaround idea of using “rayCast” as a sonor beam to detect walls in 3D did seem to work though. For example, in my demo where my flying dragon attached to the scene camera flies towards a static pyramid, I added this code:

— add a static rigid body structure to the static pyramid with its walls
Pyramid:add(craft.rigidbody, STATIC)
Pyramid:add(craft.shape.model, Pyramid.model)

— on each screen refresh send a raycast ray out from the position of the FPS camera forward to see if any walls in front of the camera; in this example, in front of the camera is vec3(0,0,1), and I’m raycasting a ray just out 1 unit forward to see if it hits the rigidbody attached to my Pyramid model wall, and if so, print “wall” in the viewer. I then reset the ray each time so that I can keep moving and detect other walls— all similar to sonor.
function update(dt)
scene:update(dt)
ray = scene.physics:raycast(v.camera.position, vec3(0,0,1), 1)
if ray then print(“wall”)
ray = “”
end
end

I’ll try using this method in my projects to detect walls in front of my character/FPS camera. I’ll probably then add code to indicate that the character/FPS can only move if raycast does not detect a wall in front of it.

If anyone else has figured out a way to detect collisions in 3D craft, I’d like to try it out.

Thanks again!

If you’re just checking to see if two objects are colliding, try this method called scene.physics:sphereCast(). Here’s the collision detection in CODEA’s built-in sample code:

    local hit1 = scene.physics:sphereCast(self.entity.position, vec3(0,-1,0), 0.52, 0.48, ~0, ~BasicPlayer1.GROUP)
    
    if hit1 and hit1.normal.y > 0.5 then
        self.grounded = true
    end
    
    local hit2 = scene.physics:sphereCast(self.entity.position, vec3(0,-1,0), 0.5, 0.52, ~0, ~BasicPlayer1.GROUP)
    
    if hit2 and hit2.normal.y < 0.5 and moveDir.z == 1 then
        self:jump()
    end

I’m not sure about its last two parameters, mask, group, but it seems to set the filter to set the priority

@SugarRay I was watching TV when I was reading your original post and didn’t realize it was for 3D. Any projects I have for 3D that have collision code just check the x,y,z values and the sizes of the objects to see if they overlap.

Thanks, @binaryblues and @dave1707. It sounds like both of you have not been able to use a specific “collide” function in 3D, either. It looks like the “sphereCast” method is similar to “rayCast” “sonar” method of detecting a 3D object in front of you (like a wall) so will either use that method or @dave1707’s method to check the x,y,z values and see if they overlap. :slight_smile:

PS,
I think I’ll use the rayCast radar/sonar beam method to detect large objects (walls) or detect if a projectile weapon or laser hits a target and will use the sphereCast method when I want to make sure my character can fit through a certain sized opening (defined by the size of the sphere at a given range) or if my character fires a weapon that fires a spread of diffuse particles towards a target.

@SugarRay @binaryblues Yeah there are two different physics systems (the original 2D one and craft.physics, i.e. 3D). Unfortunately I never got around to putting in a contact callback system for craft

As with a lot of the mismatched systems I’m trying to fix this in Codea 4. Both 2D and 3D physics are handled the same way, accessible on their own or as part of scenes and entities. Both with compound shapes, joints, sensors, contacts and queries. Hopefully it’ll make life easier once I get it out the door

Thanks, @John, @binaryblues , and @dave1707!

I’ve attached in the meantime my workaround for not having callbacks to detect collisions by using the rayCast method that I mentioned previously. However, it turned out to be a bit more complicated than the initial steps I suggested above—specifically, it required another line of code to get the origin of the ray from the camera element attached to the scene.camera before rayCasting (I figured that out after reading a Unity tutorial on the same). Anyways, at least from my initial trials, the rayCast method of detecting obstructions to movement seems to work. I’ve attached the zip of the fully edited project (credit to dave1707 for helping me refine the 3D FPS movement and for making a nice button class for movement input) and listed my commented code below. The 3D maze obj file is too large to attach in the zip file but can be downloaded for free at:

https://www.cgtrader.com/items/2215957/download-page

You’ll also need a texture for the 3D maze; I didn’t include it so feel free to choose your own texture for the walls.

Code:

-- CraftMaze

viewer.mode=FULLSCREEN
function setup()
    btn={}
    rectMode(CENTER)
    -- euler Angles of camera rotation: ex, ey, ez
    ex,ey,ez=0,0,0
    -- speed = # of units to move on vector in space with forward/back/up/down button; rY= rotation around Y axis euler degrees
    speed=0
    -- rY = rotation around Y axis in euler degrees
    rY=0
    -- rX = rotation around X axis euler degrees 
    rX=0
    -- dirSign is direction of movement: +1 forward, -1 = backward 
    dirSign = 0
    -- set the camera position (arbitrarily set -2 leftward and -40 back from maze to view maze properly)
    cameraX,cameraY,cameraZ=-2,0,-40
    scene = craft.scene()
    scene.camera.position = vec3(cameraX,cameraY,cameraZ)
    scene.camera.eulerAngles = vec3(ex,ey,ez)
    scene.sun.rotation = quat.eulerAngles(0,0,0)
    scene.ambientColor = color(90,90,90)   
    skyMaterial = scene.sky.material
    skyMaterial.horizon = color(0, 203, 255, 255)
    -- obstruction is variable then gets set if Codea finds something in front of player movement
    obstruction = ""
    
    -- create floor of maze
    createFloor()
    
    -- import 3D model of Maze(.obj format) from document folder in Codea (maze model & Stonewall texture are free downloads)
    Maze = scene:entity()
    Maze.model = craft.model(asset.documents.Maze)
    Maze.material = craft.material(asset.builtin.Materials.Standard)
    Maze.material.map = readImage(asset.documents.StoneWall)
    Maze.position = vec3(0,-0.5,0)
    Maze.scale = vec3(1,1,1)
    -- add a static rigid body structure to the maze entity so can detect when maze wall obstructs movement
    Maze:add(craft.rigidbody, STATIC)
    Maze:add(craft.shape.model, Maze.model)
    
    -- create a table called "btn" of "button" class objects & initatilize buttons
    table.insert(btn,button(WIDTH/20,150,"Left",function() rY=-.3 end))  
    table.insert(btn,button((WIDTH/20 + 110),150,"Right",function() rY=.3 end))  
    table.insert(btn,button(WIDTH - (WIDTH/20),175,"Forward",function() dirSign = 1 speed=.1 end))  
    table.insert(btn,button(WIDTH - (WIDTH/20),125,"Backward",function() dirSign = -1 speed=-.1 end))  
    table.insert(btn,button((WIDTH/20 + 55),200,"Up",function() rX=.3 end))  
    table.insert(btn,button((WIDTH/20 + 55),100,"Down",function() rX=-.3 end))  
end

function draw()
    -- draw scene, then draw buttons, then update the CameraPosition (= player in this FPS project) then move on screen
    scene:draw()
    for a,b in pairs(btn) do
        b:draw()
    end
    updateCameraPos()
    update(DeltaTime)
end

function update(dt)
    scene:update(dt)
    scene.camera.position = vec3(cameraX,cameraY,cameraZ)
    scene.camera.eulerAngles=vec3(ex,ey,ez)
end

function touched(t)
    --if screen is touched then iterate over the btn table and call the button:touched()
    for a,b in pairs(btn) do
        b:touched(t)
    end
end

function sizeChanged()
    --if screen rotates vertically need to move right sided buttons inward
    moveButtonCloser()
end

-- update CameraPos in this FirstPerson perspective game is actually what moves player in 3D environment
function updateCameraPos()
    -- camera euler y rotation (ey) = ey - rY (rY is degrees changed when hit left/right button)
    -- *Note: by Euler convention + Euler angle is counterclockwise rotation around an axis
    ey=ey-rY
    ex=ex-rX
    -- *Before actually move check to be sure there isn't any obstruction if proceed with movement at variable speed
    checkObstruction()
    -- if there is an obstruction to forward or backward movement don't update or move the camera
    if obstruction then return else
        -- x is perpendicular to z axis; calculate x vector magnitude to move based on sin of rad(ey) degrees off of z axis
        -- note: the further angled off of the z axis the greater the magnitude of perpendicular x vector
        -- then multiple by speed set as general magnitude of all hmovement
        -- cos of rad (ey) is the lefover vector to move z; when ey = 0 degrees move full vector on z
        x=speed*math.sin(math.rad(ey))
        z=speed*math.cos(math.rad(ey))
        y=speed*math.sin(math.rad(ex))
        -- cameraX is actual camera's x position 
        cameraX=cameraX+x
        cameraY=cameraY-y
        -- this line prevents camera from moving below plane (may want to remove for other projects)
        if cameraY<1 then
            cameraY=1
        end
        cameraZ=cameraZ+z    
    end
end

function moveButtonCloser()
    -- when screen held vertical need to move the right-sided buttons inwards 
    btn[3].x = (WIDTH - WIDTH/20)
    btn[4].x = (WIDTH - WIDTH/20)
    draw()
end

function createFloor()
    local c1=scene:entity()
    c1.model = craft.model.cube(vec3(50,.1,50))
    c1.position = vec3(0,0,0)
    c1.material = craft.material(asset.builtin.Materials.Standard)
    c1.material.map = readImage(asset.builtin.Surfaces.Desert_Cliff_Color)
end

function checkObstruction()
    -- first, need to actually access the camera element attached to the entity called scene.camera
    local cam = scene.camera:get(craft.camera)
    -- then pass the x,y display coordinates of the center of the camera's viewport (0.5,0.5 in fullscreen view) to screenToRay
    origin, direction = cam:screenToRay(vec2(0.5,0.5))
    -- next Codea sends out a "ray" from the camera in a forward or backward direction (based on dirSign variable from forward/backward button) a specified distance to see if there is any obstruction ("hits"); arbitrarily set distance to 0.1 since that is the speed of the movement variable set that is used to determine how far the camera moves when press forward or backward button
    obstruction = scene.physics:raycast(origin, dirSign*direction, 0.1)
end

-- useful "button" class courtesy of @dave1707
button=class()

function button:init(x,y,n,a)
    self.x=x
    self.y=y
    self.name=n  
    self.action=a  
end

function button:draw()
    fill(255)
    rect(self.x,self.y,80,40)
    fill(0)
    text(self.name,self.x,self.y)
end

function button:touched(t)
    if t.x>self.x-40 and t.x<self.x+40 and t.y>self.y-20 and t.y<self.y+20 then
        if t.state==BEGAN then
            self.action()
        end
        -- once stop pressing key, resets to no turning (rY), no fwd/bck movement, no elevation/depression mvmnt (rX)
        if t.state==ENDED then
            rY,speed,rX=0,0,0
        end
    end    
end