Compare commits

...

13 Commits

10 changed files with 494 additions and 36 deletions

BIN
data/images/measure002a.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

38
data/models/unit_cube.obj Normal file
View File

@ -0,0 +1,38 @@
# Blender 4.0.2
# www.blender.org
o Cube
v -0.500000 -0.500000 0.500000
v -0.500000 0.500000 0.500000
v -0.500000 -0.500000 -0.500000
v -0.500000 0.500000 -0.500000
v 0.500000 -0.500000 0.500000
v 0.500000 0.500000 0.500000
v 0.500000 -0.500000 -0.500000
v 0.500000 0.500000 -0.500000
vn -1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -0.0000 -1.0000 -0.0000
vn -0.0000 1.0000 -0.0000
vt 0.375000 0.000000
vt 0.625000 0.000000
vt 0.625000 0.250000
vt 0.375000 0.250000
vt 0.625000 0.500000
vt 0.375000 0.500000
vt 0.625000 0.750000
vt 0.375000 0.750000
vt 0.625000 1.000000
vt 0.375000 1.000000
vt 0.125000 0.500000
vt 0.125000 0.750000
vt 0.875000 0.500000
vt 0.875000 0.750000
s 0
f 1/1/1 2/2/1 4/3/1 3/4/1
f 3/4/2 4/3/2 8/5/2 7/6/2
f 7/6/3 8/5/3 6/7/3 5/8/3
f 5/8/4 6/7/4 2/9/4 1/10/4
f 3/11/5 7/6/5 5/8/5 1/12/5
f 8/5/6 4/13/6 2/14/6 6/7/6

BIN
data/music/bg1.xm Normal file

Binary file not shown.

View File

@ -99,7 +99,7 @@ end
function Duck:start_chase(feed)
if self.state == States.CHASE then return end
print("duck " .. self.index .. " starting chase")
-- print("duck " .. self.index .. " starting chase")
self.state = States.CHASE
self.target = feed
feed.occupied = true

View File

@ -0,0 +1,133 @@
---@class Obj
---@field model string
---@field texture string
---@field texture_size integer
---@field position Vector3
---@field rotation Vector3
---@field triangles table
local Obj = {}
local Vector3 = require "types.vector3"
Obj.__index = Obj
local function string_split(str, delimiter)
local res = {}
local i = 0
local f
local match = '(.-)' .. delimiter .. '()'
if string.find(str, delimiter) == nil then
return {str}
end
for sub, j in string.gmatch(str, match) do
i = i + 1
res[i] = sub
f = j
end
if i ~= 0 then
res[i+1] = string.sub(str, f)
end
return res
end
---parses the obj file and triangulates it for drawing
---@param model string path to obj model
---@param texture {file: string, size: number}
---@param position Vector3?
---@return Obj
function Obj.create(model, texture, position)
local pos
if position ~= nil then
pos = position
else
pos = Vector3()
end
local obj = {
model = model,
texture = texture.file,
texture_size = texture.size,
position = pos:copy(),
rotation = Vector3(),
triangles = {}
}
obj.position = obj.position:copy()
setmetatable(obj, Obj)
-- adapted from https://github.com/karai17/lua-obj/blob/master/obj_loader.lua
-- Copyright (c) 2014 Landon Manning - LManning17@gmail.com - LandonManning.com
-- MIT
local file = file_read{file = obj.model}
local lines = string_split(file, "\r?\n")
local obj_data = {
v = {},
vt = {},
f = {},
}
for _, line in ipairs(lines) do
local l = string_split(line, "%s+")
if l[1] == "v" then
local v = {
x = tonumber(l[2]),
y = tonumber(l[3]),
z = tonumber(l[4]),
}
table.insert(obj_data.v, v)
elseif l[1] == "vt" then
local vt = {
x = tonumber(l[2]) * obj.texture_size,
y = tonumber(l[3]) * obj.texture_size,
}
table.insert(obj_data.vt, vt)
elseif l[1] == "f" then
local f = {}
for i = 2, #l do
local split = string_split(l[i], "/")
local v = {}
v.v = tonumber(split[1])
if split[2] ~= "" then v.vt = tonumber(split[2]) end
-- v.vn = tonumber(split[3])
table.insert(f, v)
end
table.insert(obj_data.f, f)
end
end
for _, face in ipairs(obj_data.f) do
for i = 2, #face - 1 do
local triangle = {
v0 = obj_data.v[face[1].v],
v1 = obj_data.v[face[i].v],
v2 = obj_data.v[face[i + 1].v],
uv0 = obj_data.vt[face[1].vt],
uv1 = obj_data.vt[face[i].vt],
uv2 = obj_data.vt[face[i + 1].vt],
texture = obj.texture
}
table.insert(obj.triangles, triangle)
end
end
return obj
end
local function rotate_vertex(obj, v)
local vec = Vector3(v)
--- YXZ (yaw pitch roll) minimizes gimbal lock
-- return vec:rotate(Vector3.UP, obj.rotation.y):rotate(Vector3.LEFT, obj.rotation.x):rotate(Vector3.FORWARD, obj.rotation.z)
return vec:rotate(Vector3.UP, obj.rotation.y):rotate(Vector3.LEFT, obj.rotation.x):rotate(Vector3.FORWARD, obj.rotation.z)
end
function Obj:draw()
for _, triangle in ipairs(self.triangles) do
local newt = util.shallow_copy(triangle)
newt.v0 = rotate_vertex(self, triangle.v0) + self.position
newt.v1 = rotate_vertex(self, triangle.v1) + self.position
newt.v2 = rotate_vertex(self, triangle.v2) + self.position
draw_triangle(newt)
end
end
return Obj

View File

@ -1,5 +1,6 @@
local Vector3 = require("types.vector3")
local Signal = require("types.signal")
local AABB = require("types.aabb")
local Player = {
position = Vector3(0, 1, 0),
@ -12,10 +13,82 @@ local Player = {
yaw_speed = 0.05,
accel = 0.3,
aabb = nil,
test_intersect = nil,
ThrowPressed = Signal.new(),
}
local aabb_offset = Vector3(-0.25, -1, -0.25)
---@param aabb AABB
---@param other AABB
---@param velocity Vector3
---@return Vector3, Vector3
local function resolve_collision(aabb, other, velocity)
local my_min = aabb.min
local my_max = aabb:get_max()
local other_min = other.min
local other_max = other:get_max()
local overlap_x = math.min(my_max.x - other_min.x, other_max.x - my_min.x)
local overlap_y = math.min(my_max.y - other_min.y, other_max.y - my_min.y)
local overlap_z = math.min(my_max.z - other_min.z, other_max.z - my_min.z)
local min_overlap = math.min(overlap_x, overlap_y, overlap_z)
local new_pos = my_min:copy()
local new_vel = velocity:copy()
if min_overlap == overlap_x then
if velocity.x > 0 then
new_pos.x = other_min.x - (my_max.x - my_min.x)
elseif velocity.x < 0 then
new_pos.x = other_max.x
else
if my_min.x < other_min.x then
new_pos.x = other_min.x - (my_max.x - my_min.x)
else
new_pos.x = other_max.x
end
end
new_vel.x = 0
elseif min_overlap == overlap_y then
if velocity.y > 0 then
new_pos.y = other_min.y - (my_max.y - my_min.y)
elseif velocity.y < 0 then
new_pos.y = other_max.y
else
if my_min.y < other_min.y then
new_pos.y = other_min.y - (my_max.y - my_min.y)
else
new_pos.y = other_max.y
end
end
new_vel.y = 0
elseif min_overlap == overlap_z then
if velocity.z > 0 then
new_pos.z = other_min.z - (my_max.z - my_min.z)
elseif velocity.z < 0 then
new_pos.z = other_max.z
else
if my_min.z < other_min.z then
new_pos.z = other_min.z - (my_max.z - my_min.z)
else
new_pos.z = other_max.z
end
end
new_vel.z = 0
end
return new_pos, new_vel
end
function Player:init()
self.aabb = AABB.new(Vector3(-0.25, 0, -0.25), Vector3(0.5, 1.1, 0.5))
end
function Player:tick(ctx)
input_action{name = "left", control = "A"}
input_action{name = "right", control = "D"}
@ -34,17 +107,39 @@ function Player:tick(ctx)
local direction = ((camera_forward * forward_input) + (camera_right * strafe_input)):normalized()
local target_vel = direction * self.speed
self.velocity = self.velocity:lerp(target_vel, self.accel)
-- self.velocity = self.velocity:lerp(target_vel, self.accel)
self.velocity = target_vel:copy()
self.position = self.position + self.velocity
if self.aabb:intersects(self.test_intersect) then
local new_pos, new_vel = resolve_collision(self.aabb, self.test_intersect, self.velocity)
self.position:set(
new_pos.x - aabb_offset.x,
new_pos.y - aabb_offset.y,
new_pos.z - aabb_offset.z
)
self.aabb.min:set(
self.position.x + aabb_offset.x,
self.position.y + aabb_offset.y,
self.position.z + aabb_offset.z
)
self.velocity:sett(new_vel)
end
if input_action_just_pressed{name = "throw"} then
self.ThrowPressed:emit(self.position:copy(), camera_forward:copy())
end
-- self.aabb.min = self.position + aabb_offset
self.aabb.min:set(
self.position.x + aabb_offset.x,
self.position.y + aabb_offset.y,
self.position.z + aabb_offset.z
)
if ctx.mouse_capture then
self.yaw = self.yaw + self.mouse_sensitivity * ctx.mouse_movement.x
end
self.position = self.position + self.velocity
self.aabb:draw()
end
return Player

View File

@ -2,24 +2,28 @@ util = require "util"
local player = require "classes.player"
local Vector3 = require "types.vector3"
local List = require "types.list"
local AABB = require "types.aabb"
local test_aabb = AABB.new(
Vector3(-1, 0, 3),
Vector3(1, 2, 1)
)
local Obj = require "classes.obj"
local cube = Obj.create("models/unit_cube.obj", {file = "images/measure002a.png", size = 512}, Vector3(0, 1, 1))
local Feed = require "classes.feed"
---@type List
local feed = List()
local Duck = require "classes.duck"
---@type List
local ducks = List()
local function create_feed(position, direction)
print("?")
local f = Feed.new(position, direction)
feed:push(f)
local eligible_ducks = ducks:filter(
function (duck)
return duck.state ~= duck.STATES.CHASE
end
)
if eligible_ducks:is_empty() then return end
eligible_ducks[1]:start_chase(f)
end
local function delete_feed(f)
@ -29,21 +33,26 @@ end
local function duck_seek_feed(duck)
local eligible_feeds = feed:filter(
function (f)
return feed.occupied == false
return f.occupied == false
end
)
if eligible_feeds:is_empty() then return end
duck:start_chase(eligible_feeds[1])
duck:start_chase(
eligible_feeds:sorted(
function(a, b)
return a.position:distance_squared_to(duck.position) < b.position:distance_squared_to(duck.position)
end
):pop_front()
)
end
-- called every frame, with constant delta time
function game_tick()
-- ctx.initialization_needed is true first frame and every time dynamic reload is performed
if ctx.initialization_needed then
-- ctx.udata persists on reload
ctx.udata = {
capture = false,
}
player:init()
player.test_intersect = test_aabb
-- audio_play{audio = "music/bg1.xm", loops = true, channel = "music"}
player.ThrowPressed:connect(create_feed)
-- spawn some ducks
@ -54,9 +63,13 @@ function game_tick()
duck.SeekFeed:connect(duck_seek_feed)
duck.index = i
end
print(ducks[1].AteFeed._connections)
print(ducks[2].AteFeed._connections)
end
-- ctx.udata persists on reload
if ctx.udata == nil then
ctx.udata = {
capture = false,
}
end
ctx.mouse_capture = ctx.udata.capture
@ -80,5 +93,12 @@ function game_tick()
texture = "images/measure001a.png",
texture_region = { x = 0, y = 0, w = 512, h = 512 },
}
draw_quad(util.merge(q, params))
-- draw_quad(util.merge(q, params))
cube.position.y = math.sin(ctx.frame_number * 0.05)
-- cube.position.z = math.cos(ctx.frame_number * 0.01)
cube.rotation.x = cube.rotation.x + 0.01
cube.rotation.z = cube.rotation.z + 0.01
cube:draw()
test_aabb:draw()
end

View File

@ -0,0 +1,85 @@
local Vector3 = require "types.vector3"
---@class AABB
local AABB = {
min = Vector3(),
size = Vector3(),
}
local RED = {
r = 255,
g = 0,
b = 0,
a = 255,
}
setmetatable(AABB, AABB)
AABB.__index = AABB
---@param position Vector3
---@param size Vector3
---@return AABB
function AABB.new(position, size)
position = position or Vector3()
size = size or Vector3(1, 1, 1)
local aabb = {
min = position,
size = size,
}
return setmetatable(aabb, AABB)
end
function AABB:get_max()
return self.min + self.size
end
function AABB:draw()
local max = self:get_max()
-- bottom rectangle
draw_line_3d{start = self.min, finish = Vector3(max.x, self.min.y, self.min.z)}
draw_line_3d{start = self.min, finish = Vector3(self.min.x, self.min.y, max.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(self.min.x, self.min.y, max.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(max.x, self.min.y, self.min.z)}
-- bottom rectangle diagonal
draw_line_3d{start = self.min, finish = Vector3(max.x, self.min.y, max.z), color = RED}
-- top rectangle
draw_line_3d{start = Vector3(self.min.x, max.y, self.min.z), finish = Vector3(max.x, max.y, self.min.z)}
draw_line_3d{start = Vector3(self.min.x, max.y, self.min.z), finish = Vector3(self.min.x, max.y, max.z)}
draw_line_3d{start = Vector3(max.x, max.y, max.z), finish = Vector3(self.min.x, max.y, max.z)}
draw_line_3d{start = Vector3(max.x, max.y, max.z), finish = Vector3(max.x, max.y, self.min.z)}
-- top rectangle diagonal
draw_line_3d{start = Vector3(max.x, max.y, max.z), finish = Vector3(self.min.x, max.y, self.min.z), color = RED}
-- hull
draw_line_3d{start = self.min, finish = Vector3(self.min.x, max.y, self.min.z)}
draw_line_3d{start = self.min, finish = Vector3(max.x, max.y, self.min.z), color = RED}
draw_line_3d{start = Vector3(max.x, self.min.y, self.min.z), finish = Vector3(max.x, max.y, self.min.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, self.min.z), finish = Vector3(max.x, max.y, max.z), color = RED}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(max.x, max.y, max.z)}
draw_line_3d{start = Vector3(max.x, self.min.y, max.z), finish = Vector3(self.min.x, max.y, max.z), color = RED}
draw_line_3d{start = Vector3(self.min.x, self.min.y, max.z), finish = Vector3(self.min.x, max.y, max.z)}
draw_line_3d{start = Vector3(self.min.x, self.min.y, max.z), finish = Vector3(self.min.x, max.y, self.min.z), color = RED}
end
---returns true if the point is inside this aabb
---@param point Vector3
---@return boolean
function AABB:has_point(point)
local max = self:get_max()
return point > self.min and point < max
end
---returns true if `other` intersects this AABB
---@param other AABB
---@return boolean
function AABB:intersects(other)
local my_max = self:get_max()
local other_max = other:get_max()
return self.min <= other_max and my_max >= other.min
end
return AABB

View File

@ -68,16 +68,23 @@ function List:pop()
return table.remove(self, #self)
end
---Reduce.
---@param f function called with element, accumulator, index
---@param init any initial value of accumulator
---Removes the first element in the list and returns it.
---@return any
function List:pop_front()
return table.remove(self, 1)
end
---Reduce.
---@generic T
---@param f fun(element: any, accumulator: T, index: integer): T
---@param init T|nil initial value of accumulator
---@return T
function List:reduce(f, init)
return reduce(self, f, init)
end
---Returns a new List of all elements of this list that match the predicate function.
---@param predicate function called with element
---@param predicate fun(element: any): boolean
---@return List
function List:filter(predicate)
return filter(self, predicate)
@ -126,4 +133,33 @@ function List:is_empty()
return #self == 0
end
---Returns a (shallow) copy of this list.
---@return List
function List:copy()
return List.create(self)
end
---Returns a new copy of this list with the order of the elements shuffled around.
---@return List
function List:shuffled()
-- https://gist.github.com/Uradamus/10323382
local list = self:copy()
for i = #list, 2, -1 do
local j = math.random(i)
list[i], list[j] = list[j], list[i]
end
return list
end
---Returns a sorted copy of this list.
---@param f? fun(a: any, b: any): boolean
---@return List
function List:sorted(f)
local list = self:copy()
table.sort(list, f)
return list
end
return List

View File

@ -1,8 +1,5 @@
--- @class Vector3
--- @field x number
--- @field y number
--- @field z number
--- @alias vectorlike number[] | Vector3
--- @class Vector3
local Vector3 = {
x = 0,
y = 0,
@ -158,6 +155,20 @@ function Vector3:__tostring()
return "Vector3(" .. tostring(self.x) .. ", " .. tostring(self.y) .. ", " .. tostring(self.z) .. ")"
end
-- note: the < and <= operators of this class are component-wise rather than lexicographic (that is, htey are useful for bounds checking but are not suitable for sorting.)
function Vector3:__lt(b)
local err, other = coerce(b, true)
if err then return nil end
return self.x < other.x and self.y < other.y and self.z < other.z
end
function Vector3:__le(b)
local err, other = coerce(b, true)
if err then return nil end
return self.x <= other.x and self.y <= other.y and self.z <= other.z
end
Vector3.__index = Vector3
--------API--------
@ -223,14 +234,48 @@ function Vector3:rotated(axis, angle)
return vaxis
end
---@diagnostic disable-next-line: undefined-field
vaxis = vaxis:normalized()
local cosa = math.cos(angle)
local sina = math.sin(angle)
-- __mul is only defined for the left operand (table), numbers don't get metatables.
-- as such, the ordering of operations here is specific
local v = (self * cosa) + (vaxis * ((1 - cosa) * self:dot(vaxis))) + (vaxis:cross(self) * sina)
return Vector3(v)
return (self * cosa) + (vaxis * ((1 - cosa) * self:dot(vaxis))) + (vaxis:cross(self) * sina)
end
---In-place version of rotated.
---@param axis Vector3
---@param angle number
---@return Vector3
function Vector3:rotate(axis, angle)
local cosa = math.cos(angle)
local sina = math.sin(angle)
local dot = self:dot(axis)
local cross = axis:cross(self)
self.x = self.x * cosa + axis.x * ((1 - cosa) * dot) + cross.x * sina
self.y = self.y * cosa + axis.y * ((1 - cosa) * dot) + cross.y * sina
self.z = self.z * cosa + axis.z * ((1 - cosa) * dot) + cross.z * sina
return self
end
---In place set.
---@param x number
---@param y number
---@param z number
---@return Vector3
function Vector3:set(x, y, z)
self.x, self.y, self.z = x, y, z
return self
end
---In place set.
---@param t vectorlike
---@return Vector3
function Vector3:sett(t)
self.x, self.y, self.z = t.x, t.y, t.z
return self
end
---Returns a copy of this vector.
@ -296,12 +341,18 @@ function Vector3:horizontal()
end
---- CONSTANTS
---@type Vector3
Vector3.UP = Vector3(0, 1, 0)
Vector3.DOWN = -Vector3.UP
---@type Vector3
Vector3.DOWN = Vector3(0, -1, 0)
---@type Vector3
Vector3.FORWARD = Vector3(0, 0, -1)
Vector3.BACK = -Vector3.FORWARD
---@type Vector3
Vector3.BACK = Vector3(0, 0, 1)
---@type Vector3
Vector3.RIGHT = Vector3(1, 0, 0)
Vector3.LEFT = -Vector3.RIGHT
---@type Vector3
Vector3.LEFT = Vector3(-1, 0, 0)
-------------------
return Vector3