var os = use('os'); var io = use('io'); var transform = use('transform'); var rasterize = use('rasterize'); var time = use('time') var tilemap = use('tilemap') // Frame timing variables var frame_times = [] var frame_time_index = 0 var max_frame_samples = 60 var frame_start_time = 0 var average_frame_time = 0 var game = args[0] var video var cnf = use('accio/config') $_.start(e => { if (e.type != 'greet') return video = e.actor graphics = use('graphics', video) send(video, {kind:"window", op:"makeRenderer"}, e => { $_.start(e => { if (gameactor) return gameactor = e.actor $_.couple(gameactor) start_pipeline() }, args[0], $_) }) }, 'prosperon/sdl_video', cnf) var geometry = use('geometry') function updateCameraMatrix(camera, winW, winH) { // world→NDC def sx = 1 / camera.size[0]; def sy = 1 / camera.size[1]; def ox = camera.pos[0] - camera.size[0] * camera.anchor[0]; def oy = camera.pos[1] - camera.size[1] * camera.anchor[1]; // NDC→pixels def vx = camera.viewport.x * winW; def vy = camera.viewport.y * winH; def vw = camera.viewport.width * winW; def vh = camera.viewport.height * winH; // final “mat” coefficients // [ a 0 c ] // [ 0 e f ] // [ 0 0 1 ] camera.a = sx * vw; camera.c = vx - camera.a * ox; camera.e = -sy * vh; camera.f = vy + vh + sy * vh * oy; // and store the inverses so we can go back cheaply camera.ia = 1 / camera.a; camera.ic = -camera.c * camera.ia; camera.ie = 1 / camera.e; camera.if = -camera.f * camera.ie; } //---- forward transform ---- function worldToScreenPoint(pos, camera) { return { x: camera.a * pos[0] + camera.c, y: camera.e * pos[1] + camera.f }; } //---- inverse transform ---- function screenToWorldPoint(pos, camera) { return { x: camera.ia * pos[0] + camera.ic, y: camera.ie * pos[1] + camera.if }; } //---- rectangle (two corner) ---- function worldToScreenRect(rect, camera) { // map bottom-left and top-right def x1 = camera.a * rect.x + camera.c; def y1 = camera.e * rect.y + camera.f; def x2 = camera.a * (rect.x + rect.width) + camera.c; def y2 = camera.e * (rect.y + rect.height) + camera.f; // pick mins and abs deltas def x0 = x1 < x2 ? x1 : x2; def y0 = y1 < y2 ? y1 : y2; return { x: x0, y: y0, width: x2 > x1 ? x2 - x1 : x1 - x2, height: y2 > y1 ? y2 - y1 : y1 - y2 }; } var camera = { size: [640,480],//{width:500,height:500}, // pixel size the camera "sees", like its resolution pos: [250,250],//{x:0,y:0}, // where it is fov:50, near_z:0, far_z:1000, viewport: {x:0,y:0,width:1,height:1}, // viewport it appears on screen ortho:true, anchor:[0.5,0.5],//{x:0.5,y:0.5}, rotation:[0,0,0,1], surface: undefined } var util = use('util') var cammy = util.camera_globals(camera) var graphics var gameactor var images = {} var renderer_commands = [] // Convert high-level draw commands to low-level renderer commands function translate_draw_commands(commands) { if (!graphics) return updateCameraMatrix(camera,500,500) renderer_commands.length = 0 commands.forEach(function(cmd) { if (cmd.material && cmd.material.color) { renderer_commands.push({ op: "set", prop: "drawColor", value: cmd.material.color }) } switch(cmd.cmd) { case "draw_rect": cmd.rect = worldToScreenRect(cmd.rect, camera) // Handle rectangles with optional rounding and thickness if (cmd.opt && cmd.opt.radius && cmd.opt.radius > 0) { // Rounded rectangle var thickness = (cmd.opt.thickness == 0) ? 0 : (cmd.opt.thickness || 1) var raster_result = rasterize.round_rect(cmd.rect, cmd.opt.radius, thickness) if (raster_result.type == 'rect') { renderer_commands.push({ op: "fillRect", data: {rect: raster_result.data} }) } else if (raster_result.type == 'rects') { raster_result.data.forEach(function(rect) { renderer_commands.push({ op: "fillRect", data: {rect: rect} }) }) } } else if (cmd.opt && cmd.opt.thickness && cmd.opt.thickness > 0) { // Outlined rectangle var raster_result = rasterize.outline_rect(cmd.rect, cmd.opt.thickness) if (raster_result.type == 'rect') { renderer_commands.push({ op: "fillRect", data: {rect: raster_result.data} }) } else if (raster_result.type == 'rects') { renderer_commands.push({ op: "rects", data: {rects: raster_result.data} }) } } else { renderer_commands.push({ op: "fillRect", data: {rect: cmd.rect} }) } break case "draw_circle": case "draw_ellipse": cmd.pos = worldToScreenPoint(cmd.pos, camera) // Rasterize ellipse to points or rects var radii = cmd.radii || [cmd.radius, cmd.radius] var raster_result = rasterize.ellipse(cmd.pos, radii, cmd.opt || {}) if (raster_result.type == 'points') { renderer_commands.push({ op: "point", data: {points: raster_result.data} }) } else if (raster_result.type == 'rects') { // Use 'rects' operation for multiple rectangles renderer_commands.push({ op: "rects", data: {rects: raster_result.data} }) } break case "draw_line": renderer_commands.push({ op: "line", data: {points: cmd.points.map(p => worldToScreenPoint(p, camera))} }) break case "draw_point": cmd.pos = worldToScreenPoint(cmd.pos, camera) renderer_commands.push({ op: "point", data: {points: [cmd.pos]} }) break case "draw_image": var img = graphics.texture(cmd.image) var gpu = img.gpu if (!gpu) break cmd.rect.width ??= img.width cmd.rect.height ??= img.height cmd.rect = worldToScreenRect(cmd.rect, camera) renderer_commands.push({ op: "texture", data: { texture_id: gpu.id, dst: cmd.rect, src: img.rect } }) break case "draw_text": if (!cmd.text) break if (!cmd.pos) break var rect = worldToScreenRect({x:cmd.pos.x, y:cmd.pos.y, width:8, height:8}, camera, 500,500) var pos = {x: rect.x, y: rect.y} renderer_commands.push({ op: "debugText", data: { pos, text: cmd.text } }) break case "tilemap": var texid tilemap.for(cmd.tilemap, (tile,{x,y}) => { if (!texid) texid = graphics.texture(tile) return graphics.texture(tile) }) var geom = geometry.tilemap_to_data(cmd.tilemap) if (texid.gpu) geom.texture_id = texid.gpu.id renderer_commands.push({ op: "geometry_raw", data: geom }) break } }) return renderer_commands } var parseq = use('parseq', $_.delay) // Wrap `send(actor,msg,cb)` into a parseq “requestor” // • on success: cb(data) → value=data, reason=undefined // • on failure: cb(undefined,err) function rpc_req(actor, msg) { return (cb, _) => { send(actor, msg, data => { if (data.error) cb(undefined, data) else cb(data) }) } } var game_rec = parseq.sequence([ rpc_req(gameactor, {kind:'update', dt:1/60}), rpc_req(gameactor, {kind:'draw'}) ]) var pending_draw = null var pending_next = null var last_time = time.number() var frames = [] var frame_avg = 0 var input = use('input') var input_state = { poll: 1/60 } // 1) input runs completely independently function poll_input() { send(video, {kind:'input', op:'get'}, evs => { for (var ev of evs) { if (ev.type == 'quit') $_.stop() if (ev.type.includes('mouse')) { if (ev.pos) ev.pos = screenToWorldPoint(ev.pos, camera, 500,500) if (ev.d_pos) ev.d_pos.y *= -1 } if (ev.type.includes('key')) { if (ev.key) ev.key = input.keyname(ev.key) } } send(gameactor, evs) }) $_.delay(poll_input, input_state.poll) } // 2) helper to build & send a batch, then call done() function create_batch(draw_cmds, done) { def batch = [ {op:'set', prop:'drawColor', value:[0.1,0.1,0.15,1]}, {op:'clear'} ] if (draw_cmds && draw_cmds.length) batch.push(...translate_draw_commands(draw_cmds)) batch.push( {op:'set', prop:'drawColor', value:[1,1,1,1]}, {op:'debugText', data:{pos:{x:10,y:10}, text:`Fps: ${(1/frame_avg).toFixed(2)}`}}, {op:'present'} ) send(video, {kind:'renderer', op:'batch', data:batch}, () => { def now = time.number() def dt = now - last_time last_time = now frames.push(dt) if (frames.length > 60) frames.shift() let sum = 0 for (let f of frames) sum += f frame_avg = sum / frames.length done(dt) }) } // 3) kick off the very first update→draw function start_pipeline() { poll_input() send(gameactor, {kind:'update', dt:1/60}, () => { send(gameactor, {kind:'draw'}, cmds => { pending_draw = cmds render_step() }) }) } function render_step() { // a) fire off the next update→draw immediately def dt = time.number() - last_time send(gameactor, {kind:'update', dt:1/60}, () => send(gameactor, {kind:'draw'}, cmds => pending_next = cmds) ) // c) render the current frame create_batch(pending_draw, ttr => { // time to render // only swap in when there's a new set of commands if (pending_next) { pending_draw = pending_next pending_next = null } // d) schedule the next render step def render_dur = time.number() - last_time def wait = Math.max(0, 1/60 - ttr) $_.delay(render_step, 0) }) } $_.receiver(e => { switch(e.op) { case 'resolution': log.console(json.encode(e)) send(video, { kind:'renderer', op:'set', prop:'logicalPresentation', value: {...e} }) break } })