Skip to content

Instantly share code, notes, and snippets.

@robert-ryu7
Last active June 8, 2021 12:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save robert-ryu7/02b03ddbd56c1eafcd06cfae56b7a9ac to your computer and use it in GitHub Desktop.
Save robert-ryu7/02b03ddbd56c1eafcd06cfae56b7a9ac to your computer and use it in GitHub Desktop.
SCRIPT-8
// title: Ray Caster
class V {
_argumentsToXY(args) {
return args.length === 1 ? args[0] : { x: args[0], y: args[1] }
}
constructor() {
const { x, y } = this._argumentsToXY(arguments)
this.x = x
this.y = y
}
add() {
const { x, y } = this._argumentsToXY(arguments)
return new V(this.x + x, this.y + y)
}
sub() {
const { x, y } = this._argumentsToXY(arguments)
return new V(this.x - x, this.y - y)
}
multiply() {
if (arguments.length === 1 && typeof arguments[0] === 'number') {
return new V(this.x * arguments[0], this.y * arguments[0])
}
const { x, y } = this._argumentsToXY(arguments)
return new V(this.x * x, this.y * y)
}
floor() {
return new V(Math.floor(this.x), Math.floor(this.y))
}
mod() {
return new V(this.x % 1, this.y % 1)
}
rotate(angle) {
const cos = Math.cos(angle)
const sin = Math.sin(angle)
return new V(this.x * cos - this.y * sin, this.x * sin + this.y * cos)
}
get half() {
return new V(this.x / 2, this.y / 2)
}
}
class Rect {
_argumentsToPosSize(args) {
return args.length === 1
? { pos: new V(args[0].pos), size: new V(args[0].size) }
: args.length === 2
? { pos: new V(args[0]), size: new V(args[1]) }
: {
pos: new V(args[0], args[1]),
size: new V(args[2], args[3])
}
}
constructor() {
const { pos, size } = this._argumentsToPosSize(arguments)
this.pos = pos
this.size = size
}
grow(sizeDelta) {
return new Rect(this.pos.sub(sizeDelta.half), this.size.add(sizeDelta))
}
get center() {
return new V(this.pos.x + this.size.x / 2, this.pos.y + this.size.y / 2)
}
}
const getRayVsRectCollisionPoint = (pos, velocity, rect) => {
const A = rect.pos
const B = rect.pos.add(rect.size)
let near, far, remainingVelocity
if (velocity.y === 0) {
if (velocity.x === 0) return null
if (pos.y <= A.y || pos.y >= B.y) return null
let [tx1, tx2] = [
(A.x - pos.x) / velocity.x,
(B.x - pos.x) / velocity.x
].sort((a, b) => a - b)
near = tx1
far = tx2
if (far <= 0) return null
if (near < 0 || near >= 1) return null
remainingVelocity = new V(0, 0)
} else if (velocity.x === 0) {
if (velocity.y === 0) return null
if (pos.x <= A.x || pos.x >= B.x) return null
let [ty1, ty2] = [
(A.y - pos.y) / velocity.y,
(B.y - pos.y) / velocity.y
].sort((a, b) => a - b)
near = ty1
far = ty2
if (far <= 0) return null
if (near < 0 || near >= 1) return null
remainingVelocity = new V(0, 0)
} else {
let [tx1, tx2] = [
(A.x - pos.x) / velocity.x,
(B.x - pos.x) / velocity.x
].sort((a, b) => a - b)
let [ty1, ty2] = [
(A.y - pos.y) / velocity.y,
(B.y - pos.y) / velocity.y
].sort((a, b) => a - b)
if (tx1 > ty2 || ty1 > tx2) return null
far = tx2 > ty2 ? ty2 : tx2
near = tx1 > ty1 ? tx1 : ty1
if (far <= 0) return null
if (near < 0 || near >= 1) return null
remainingVelocity = velocity
.multiply(tx1 > ty1 ? new V(0, 1) : new V(1, 0))
.multiply(new V(1 - near, 1 - near))
}
return {
near,
point: pos.add(velocity.multiply(near)),
remainingVelocity
}
}
const getDynamicRectVsRectCollisionPoint = (
dynamicRect,
velocityVector,
rect
) => {
return getRayVsRectCollisionPoint(
dynamicRect.center,
velocityVector,
rect.grow(dynamicRect.size)
)
}
const resolveCollisions = (player, velocityVector, obstacles) => {
const halfPlayerSize = player.size.half
let nextPlayer = new Rect(player)
let nextVelocityVector = new V(velocityVector)
for (let j = 0; j < 2; j++) {
const collisions = obstacles
.map(obstacle =>
getDynamicRectVsRectCollisionPoint(
nextPlayer,
nextVelocityVector,
new Rect(obstacle)
)
)
.filter(a => a)
.sort((a, b) => a.near - b.near)
const collision = collisions[0]
if (!collision) break
nextPlayer.pos = collision.point.sub(halfPlayerSize)
nextVelocityVector = collision.remainingVelocity
}
nextPlayer.pos = nextPlayer.pos.add(nextVelocityVector)
return nextPlayer
}
const getCell = (v, array) => {
if (v.x < 0 || v.y < 0) return -1
const row = array[Math.floor(v.y)]
if (row === undefined) return -1
const cell = row[Math.floor(v.x)]
if (cell === undefined) return -1
return cell
}
const calculateVisionRay = (x, y, cx, cy, dx, dy, dir, iMax, level) => {
let length = 0
let tX = 0
if (dir.x === 1 && dir.y === 0) {
let iX = cx
for (let i = 1; i < iMax; i++) {
if (getCell(new V(iX + 0.5, cy), level)) {
break
}
iX += 1
}
length = iX - x
} else if (dir.x === -1 && dir.y === 0) {
let iX = cx
for (let i = 1; i < iMax; i++) {
if (getCell(new V(iX - 0.5, cy), level)) {
break
}
iX -= 1
}
length = x - iX
} else if (dir.x === 0 && dir.y === 1) {
let iY = cy
for (let i = 1; i < iMax; i++) {
if (getCell(new V(cx, iY + 0.5), level)) {
break
}
iY += 1
}
length = iY - y
} else if (dir.x === 0 && dir.y === -1) {
let iY = cy
for (let i = 1; i < iMax; i++) {
if (getCell(new V(cx, iY - 0.5), level)) {
break
}
iY -= 1
}
length = y - iY
} else {
const tan = dir.y / dir.x
const tileStepX = dir.x > 0 ? 1 : dir.x < 0 ? -1 : 0
const tileStepY = dir.y > 0 ? 1 : dir.y < 0 ? -1 : 0
let xIntercept = new V(
cx + (tileStepX > 0 ? 1 : 0),
y + (tileStepX > 0 ? 1 - dx : dx) * tan * tileStepX
)
let yIntercept = new V(
x + ((tileStepY > 0 ? 1 - dy : dy) / tan) * tileStepY,
cy + (tileStepY > 0 ? 1 : 0)
)
for (let i = 1; i < iMax; i++) {
if (getCell(xIntercept.add(tileStepX * 0.5, 0), level)) {
break
}
xIntercept = xIntercept.add(tileStepX, tan * tileStepX)
}
for (let i = 1; i < iMax; i++) {
if (getCell(yIntercept.add(0, tileStepY * 0.5), level)) {
break
}
yIntercept = yIntercept.add(tileStepY / tan, tileStepY)
}
const xILength = Math.abs((xIntercept.x - x) / dir.x)
const yILength = Math.abs((yIntercept.x - x) / dir.x)
if (xILength < yILength) {
length = xILength
tX = xIntercept.y % 1
} else {
length = yILength
tX = yIntercept.x % 1
}
}
return { length, tX }
}
const calculateVisionRays = (
x,
y,
cx,
cy,
dx,
dy,
dir,
iMax,
level,
screen
) => {
const fov = Math.PI / 3
const fppWidth = screen.size.x
const fppHeight = screen.size.y
const fppHalfHeight = screen.center.y
const steps = fppWidth
const rayStep = fov / (steps - 1)
const rayDirOffset = fov * 0.5
const rayDirStart = dir.rotate(-rayDirOffset)
const b = fppHeight * 0.75
const lines = []
for (let i = 0; i < steps; i++) {
const rayDir = rayDirStart.rotate(rayStep * i)
const { length, tX } = calculateVisionRay(
x,
y,
cx,
cy,
dx,
dy,
rayDir,
iMax,
level
)
const adjustedLength = new V(length, 0).rotate(rayStep * i - rayDirOffset).x
const lineHeight = b / adjustedLength
const halfLineHeight = lineHeight / 2
lines.push([
i,
fppHalfHeight - halfLineHeight,
fppHalfHeight + halfLineHeight,
4 + (clamp(tX, 0.025, 0.975) === tX ? 0 : 1) + (3 * length) / (iMax - 1)
])
}
return lines
}
function createPreCalculatedThresholdMatrix(n) {
function r(array) {
const size = array.length * 2
const matrix = range(size).map(() => range(size))
for (let y = 0; y < size; y++)
for (let x = 0; x < size; x++) {
let value,
base = 4 * array[y % array.length][x % array.length]
if (y < array.length && x < array.length) value = base
else if (x < array.length) value = base + 3
else if (y < array.length) value = base + 2
else value = base + 1
matrix[y][x] = value
}
return matrix
}
let array = []
for (let m = 1; m <= n; m++) {
if (m === 1) {
array = [
[0, 2],
[3, 1]
]
} else {
array = r(array)
}
}
for (let y = 0; y < array.length; y++)
for (let x = 0; x < array.length; x++)
array[y][x] = (array[y][x] + 1) / (array.length * array.length) - 0.5
return array
}
function createOrderedDitheringFunc(n) {
const thresholdMatrix = createPreCalculatedThresholdMatrix(n)
const size = thresholdMatrix.length
return (x, y, buffer) => {
const factor = thresholdMatrix[y % size][x % size]
const attempt = buffer[x][y] + factor
buffer[x][y] = clamp(Math.round(attempt), 0, 7)
}
}
const dither = createOrderedDitheringFunc(2)
function bufferLine(x, y1, y2, color, buffer) {
y1 = Math.round(y1)
y2 = Math.round(y2)
for (let i = y1; i < y2; i++) {
buffer[x][i] = color
}
}
const screenSize = 128
const mapSize = 32
const level = [
[1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1]
]
const moveSpeedInSquares = 2 // per second
const rotationSpeedInDegrees = 120 // per second
const moveSpeed = moveSpeedInSquares / 1000 // squares per millisecond
const rotationSpeed = (rotationSpeedInDegrees * Math.PI) / 180 / 1000 // radians per millisecond
const mapCellSize = mapSize / level.length
const playerSize = 0.5
const iMax = 8
init = state => {
state.playerPos = [1.5 - playerSize / 2, 6.5 - playerSize / 2]
state.playerCell = [
Math.floor(state.playerPos[0]),
Math.floor(state.playerPos[1])
]
state.playerDir = [1.0, 0.0]
state.debug = {}
}
update = (state, input, elapsed) => {
const obstacles = []
let velocity = new V(0, 0)
let nextPlayerDir = state.playerDir
if (input.up) {
const delta = elapsed * moveSpeed
velocity = new V(state.playerDir[0] * delta, state.playerDir[1] * delta)
}
if (input.down) {
const delta = elapsed * moveSpeed
velocity = new V(state.playerDir[0] * -delta, state.playerDir[1] * -delta)
}
if (input.right) {
const delta = elapsed * rotationSpeed
nextPlayerDir = [
state.playerDir[0] * Math.cos(delta) -
state.playerDir[1] * Math.sin(delta),
state.playerDir[0] * Math.sin(delta) +
state.playerDir[1] * Math.cos(delta)
]
}
if (input.left) {
const delta = elapsed * rotationSpeed
nextPlayerDir = [
state.playerDir[0] * Math.cos(-delta) -
state.playerDir[1] * Math.sin(-delta),
state.playerDir[0] * Math.sin(-delta) +
state.playerDir[1] * Math.cos(-delta)
]
}
const { x: x1, y: y1 } = new V(state.playerPos[0], state.playerPos[1])
const { x: x2, y: y2 } = new V(state.playerPos[0], state.playerPos[1]).add(
velocity
)
const range = {
start: new V(x1 < x2 ? x1 : x2, y1 < y2 ? y1 : y2).floor(),
end: new V(x1 < x2 ? x2 : x1, y1 < y2 ? y2 : y1)
.add(playerSize, playerSize)
.floor()
}
for (let y = range.start.y; y <= range.end.y; y++)
for (let x = range.start.x; x <= range.end.x; x++)
level[y][x] && obstacles.push(new Rect(x, y, 1, 1))
const player = resolveCollisions(
new Rect(...state.playerPos, playerSize, playerSize),
velocity,
obstacles.filter(a => a)
)
const { x, y } = player.center
const { x: cx, y: cy } = player.center.floor()
const { x: dx, y: dy } = player.center.mod()
const lines = calculateVisionRays(
x,
y,
cx,
cy,
dx,
dy,
new V(nextPlayerDir[0], nextPlayerDir[1]),
iMax,
level,
new Rect(0, 0, screenSize, screenSize)
)
state.lines = lines
state.playerCell = [Math.floor(player.pos.x), Math.floor(player.pos.y)]
state.playerPos = [player.pos.x, player.pos.y]
state.playerDir = nextPlayerDir
}
draw = state => {
clear()
// main
const buffer = range(128).map(() =>
range(128).map((a, i) => (i < 128 / 2 ? 7 : 7.5 - i / 128))
)
state.lines.forEach(args => {
const [x, y1, y2, color] = args
bufferLine(x, y1, y2, color, buffer)
})
for (let y = 0; y < 128; y++) {
for (let x = 0; x < 128; x++) {
dither(x, y, buffer)
setPixel(x, y, buffer[x][y])
}
}
// minimap
for (let y = 0; y < level.length; y++)
for (let x = 0; x < level[0].length; x++)
if (level[y][x] !== 0)
rectFill(x * mapCellSize, y * mapCellSize, mapCellSize, mapCellSize, 5)
else
rectFill(x * mapCellSize, y * mapCellSize, mapCellSize, mapCellSize, 6)
const scaledPlayerPos = new V(
state.playerPos[0] * mapCellSize,
state.playerPos[1] * mapCellSize
)
const scaledPlayerSize = playerSize * mapCellSize
const scaledPlayerHalfSize = scaledPlayerSize / 2
rectFill(
scaledPlayerPos.x,
scaledPlayerPos.y,
scaledPlayerSize,
scaledPlayerSize,
4
)
line(
scaledPlayerPos.x + scaledPlayerHalfSize,
scaledPlayerPos.y + scaledPlayerHalfSize,
scaledPlayerPos.x +
scaledPlayerHalfSize +
state.playerDir[0] * 1 * mapCellSize,
scaledPlayerPos.y +
scaledPlayerHalfSize +
state.playerDir[1] * 1 * mapCellSize,
3
)
}
{
"iframeVersion": "0.1.280",
"lines": [
77,
295,
158,
0,
0,
0,
0,
0
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment