1 Commits

Author SHA1 Message Date
07d2d8ea05 Work In Progress 2020-03-25 16:16:47 -05:00
196 changed files with 3298 additions and 16748 deletions

View File

@@ -1,37 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
name: Ruby
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
# change this to (see https://github.com/ruby/setup-ruby#versioning):
# uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- name: Install Gosu dependencies
run: |
sudo apt-get update -y -qq
sudo apt-get install -y libsdl2-dev libgl1-mesa-dev libfontconfig1-dev libopenal-dev libsndfile1-dev libmpg123-dev cmake:
- name: Install dependencies
run: bundle install
- name: Run tests
run: bundle exec rake

View File

@@ -1,8 +0,0 @@
Style/StringLiterals:
EnforcedStyle: double_quotes
Metrics/MethodLength:
Max: 40
Style/EmptyMethod:
EnforcedStyle: expanded

11
Gemfile
View File

@@ -1,15 +1,8 @@
# frozen_string_literal: true
source "https://rubygems.org" source "https://rubygems.org"
gem "cyberarm_engine", git: "https://github.com/cyberarm/cyberarm_engine"
gem "i18n"
gem "nokogiri", ">= 1.11.0.rc1"
gem "opengl-bindings", require: "opengl" gem "opengl-bindings", require: "opengl"
gem "rake" gem "cyberarm_engine", git: "https://github.com/cyberarm/cyberarm_engine"
gem "nokogiri", ">= 1.11.0.rc1"
group(:packaging) do group(:packaging) do
gem "excon"
gem "ocra" gem "ocra"
gem "releasy", github: "gosu/releasy"
gem "rubyzip"
end end

View File

@@ -1,57 +1,32 @@
GIT GIT
remote: https://github.com/cyberarm/cyberarm_engine remote: https://github.com/cyberarm/cyberarm_engine
revision: d1d87db070578fefe97f275b63157b4212a44a89 revision: d8551c7428da98bb7da76c138e5fbde50ef0137f
specs: specs:
cyberarm_engine (0.22.0) cyberarm_engine (0.13.1)
excon (~> 0.88) gosu (~> 0.15.0)
gosu (~> 1.1)
gosu_more_drawables (~> 0.3)
GIT
remote: https://github.com/gosu/releasy.git
revision: e8a24c079c4930c6ddbab17fc444027ba41491ca
specs:
releasy (0.2.3)
bundler (>= 1.2.1)
cri (~> 2.1.0)
ocra (~> 1.3.0)
rake (>= 0.9.2.2)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
concurrent-ruby (1.1.10) gosu (0.15.1)
cri (2.1.0) gosu (0.15.1-x64-mingw32)
excon (0.96.0) mini_portile2 (2.4.0)
gosu (1.4.5) nokogiri (1.11.0.rc1)
gosu_more_drawables (0.3.1) mini_portile2 (~> 2.4.0)
i18n (1.12.0) nokogiri (1.11.0.rc1-x64-mingw32)
concurrent-ruby (~> 1.0) mini_portile2 (~> 2.4.0)
mini_portile2 (2.8.1)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
ocra (1.3.11) ocra (1.3.11)
opengl-bindings (1.6.13) opengl-bindings (1.6.9)
racc (1.6.2)
rake (13.0.6)
rubyzip (2.3.2)
PLATFORMS PLATFORMS
ruby ruby
x64-mingw-ucrt
x64-mingw32 x64-mingw32
DEPENDENCIES DEPENDENCIES
cyberarm_engine! cyberarm_engine!
excon
i18n
nokogiri (>= 1.11.0.rc1) nokogiri (>= 1.11.0.rc1)
ocra ocra
opengl-bindings opengl-bindings
rake
releasy!
rubyzip
BUNDLED WITH BUNDLED WITH
2.4.1 2.1.4

View File

@@ -1,43 +1,12 @@
![Ruby](https://github.com/cyberarm/i-mic-fps/workflows/Ruby/badge.svg)
![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/cyberarm/i-mic-fps?include_prereleases)
![GitHub repo size](https://img.shields.io/github/repo-size/cyberarm/i-mic-fps)
# I-MIC FPS # I-MIC FPS
![logo](https://raw.githubusercontent.com/cyberarm/i-mic-fps/master/svg/logo.svg) An endeavor to create a multiplayer first-person-shooter in pure Ruby; Using C extensions only for Rendering, Sound, and Input. ([Gosu](https://libgosu.org) and [opengl-bindings](https://github.com/vaiorabbit/ruby-opengl/))
Creating a multiplayer first-person-shooter in pure Ruby; Using C extensions only for Rendering, Sound, and Input. ([Gosu](https://libgosu.org) and [opengl-bindings](https://github.com/vaiorabbit/ruby-opengl/))
![screenshot](https://raw.githubusercontent.com/cyberarm/i-mic-fps/master/screenshots/screenshot-game.png)
## Using ## Using
Ruby 3.0+ interpeter with support for the Gosu game library C extension. Requires a Ruby runtime that supports the gosu and opengl-bindings C-extensions (truffleruby 1.0.0-rc12 did not work when tested. Rubinus was not tested.)
* Clone or download this repo * Clone or download this repo
* `bundle install` * `bundle install`
* `bundle exec ruby i-mic-fps.rb [options]` * `bundle exec ruby i-mic-fps.rb [options]`
### System Requirements
| Minimum | |
| :------ | ----------------------: |
| OS | Windows 10 or GNU/Linux |
| CPU | Intel Core i5-3320M |
| RAM | 512 MB |
| GPU | OpenGL 3.30 Capable |
| Storage | To Be Determined |
| Network | To Be Determined |
| Display | 1280x720 |
| Recommended | |
| :---------- | ----------------------------: |
| OS | Windows 10 or GNU/Linux |
| CPU | AMD Ryzen 5 3600 |
| RAM | 1 GB+ |
| GPU | AMD Radeon RX 5700 XT |
| Storage | To Be Determined (< 4 GB) |
| Network | Broadband Internet Connection |
| Display | 1920x1080 60Hz |
Note: Recommended CPU and GPU are those of the primary development system and are overkill at this point.
### Options ### Options
* `--native` - Launch in fullscreen using primary displays resolution * `--native` - Launch in fullscreen using primary displays resolution
* `--profile` - Run ruby-prof profiler * `--profile` - Run ruby-prof profiler

View File

@@ -1,18 +1,10 @@
# frozen_string_literal: true
require "json"
require "tmpdir"
require "fileutils"
require "zip"
require "excon"
require "releasy" require "releasy"
require "bundler/setup" # Releasy requires that your application uses bundler. require 'bundler/setup' # Releasy requires that your application uses bundler.
require_relative "lib/version" require_relative "lib/version"
Releasy::Project.new do Releasy::Project.new do
name IMICFPS::NAME name "I-MIC FPS"
version IMICFPS::VERSION version "#{IMICFPS::VERSION}"
executable "i-mic-fps.rb" executable "i-mic-fps.rb"
files ["lib/**/*.*", "assets/**/*.*", "blends/**/*.*", "shaders/**/*.*", "static/**/*.*", "maps/**/*.*", "data/**/*.*"] files ["lib/**/*.*", "assets/**/*.*", "blends/**/*.*", "shaders/**/*.*", "static/**/*.*", "maps/**/*.*", "data/**/*.*"]
@@ -20,7 +12,7 @@ Releasy::Project.new do
verbose verbose
add_build :windows_folder do add_build :windows_folder do
icon "static/icon.ico" # icon "assets/icon.ico"
executable_type :console # Assuming you don't want it to run with a console window. executable_type :console # Assuming you don't want it to run with a console window.
add_package :exe # Windows self-extracting archive. add_package :exe # Windows self-extracting archive.
end end

View File

@@ -1,39 +1,34 @@
# frozen_string_literal: true require "gosu"
require_relative "lib/objects/text"
begin
require_relative "../cyberarm_engine/lib/cyberarm_engine"
rescue LoadError
require "cyberarm_engine"
end
class Window < Gosu::Window class Window < Gosu::Window
def initialize def initialize
super(Gosu.screen_width, Gosu.screen_height, fullscreen: true) super(Gosu.screen_width, Gosu.screen_height, fullscreen: true)
CyberarmEngine::Window.instance = self $window = self
@size = 50 @size = 50
@slope = 250 @slope = 250
@color_step = 10 @color_step = 10
@base_color = Gosu::Color.rgb(255, 127, 0) @base_color = Gosu::Color.rgb(255, 127, 0)
@title = CyberarmEngine::Text.new("I-MIC FPS", color: Gosu::Color.rgb(255, 127, 0), size: 100, x: 0, y: 15, alignment: :center) @title = Text.new("I-MIC FPS", color: Gosu::Color.rgb(255,127,0), size: 100, x: 0, y: 15, alignment: :center)
@singleplayer = CyberarmEngine::Text.new("Singleplayer", color: Gosu::Color.rgb(0, 127, 127), size: 50, x: 0, y: 150, alignment: :center) @singleplayer = Text.new("Singleplayer", color: Gosu::Color.rgb(0,127,127), size: 50, x: 0, y: 150, alignment: :center)
end end
def draw def draw
@background ||= Gosu.record(Gosu.screen_width, Gosu.screen_height) do @background ||= Gosu.record(Gosu.screen_width, Gosu.screen_height) do
((Gosu.screen_height + @slope) / @size).times do |i| ((Gosu.screen_height+@slope)/@size).times do |i|
fill_quad( fill_quad(
0, i * @size, 0, i*@size,
0, @slope + (i * @size), 0, @slope+(i*@size),
Gosu.screen_width / 2, (-@slope) + (i * @size), Gosu.screen_width/2, (-@slope)+(i*@size),
Gosu.screen_width / 2, i * @size, Gosu.screen_width/2, i*@size,
Gosu::Color.rgba(@base_color.red - i * @color_step, @base_color.green - i * @color_step, @base_color.blue - i * @color_step, 200) Gosu::Color.rgba(@base_color.red-i*@color_step, @base_color.green-i*@color_step, @base_color.blue-i*@color_step, 200)
) )
fill_quad( fill_quad(
Gosu.screen_width, i * @size, Gosu.screen_width, i*@size,
Gosu.screen_width, @slope + (i * @size), Gosu.screen_width, @slope+(i*@size),
Gosu.screen_width / 2, (-@slope) + (i * @size), Gosu.screen_width/2, (-@slope)+(i*@size),
Gosu.screen_width / 2, i * @size, Gosu.screen_width/2, i*@size,
Gosu::Color.rgba(@base_color.red - i * @color_step, @base_color.green - i * @color_step, @base_color.blue - i * @color_step, 200) Gosu::Color.rgba(@base_color.red-i*@color_step, @base_color.green-i*@color_step, @base_color.blue-i*@color_step, 200)
) )
end end
end end
@@ -42,8 +37,8 @@ class Window < Gosu::Window
# Box # Box
draw_rect( draw_rect(
Gosu.screen_width / 4, 0, Gosu.screen_width/4, 0,
Gosu.screen_width / 2, Gosu.screen_height, Gosu.screen_width/2, Gosu.screen_height,
Gosu::Color.rgba(100, 100, 100, 150) Gosu::Color.rgba(100, 100, 100, 150)
# Gosu::Color.rgba(@base_color.red+@color_step, @base_color.green+@color_step, @base_color.blue+@color_step, 200) # Gosu::Color.rgba(@base_color.red+@color_step, @base_color.green+@color_step, @base_color.blue+@color_step, 200)
) )
@@ -55,21 +50,21 @@ class Window < Gosu::Window
# Cursor # Cursor
fill_quad( fill_quad(
mouse_x, mouse_y, mouse_x, mouse_y,
mouse_x + 16, mouse_y + 16, mouse_x+16, mouse_y+16,
mouse_x, mouse_y + 16, mouse_x, mouse_y+16,
mouse_x, mouse_y + 16, mouse_x, mouse_y+16,
Gosu::Color::RED, Float::INFINITY Gosu::Color::RED, Float::INFINITY
) )
end end
def fill_quad(x1, y1, x2, y2, x3, y3, x4, y4, color = Gosu::Color::WHITE, z = 0, mode = :default) def fill_quad(x1, y1, x2, y2, x3, y3, x4, y4, color = Gosu::Color::WHITE, z = 0, mode = :default)
draw_quad( draw_quad(
x1, y1, color, x1,y1, color,
x2, y2, color, x2,y2, color,
x3, y3, color, x3,y3, color,
x4, y4, color, x4,y4, color,
z, mode z, mode
) )
end end
def button_up(id) def button_up(id)

View File

@@ -1,11 +1,9 @@
# frozen_string_literal: true
origin = entity.position origin = entity.position
on.entity_moved do |event| on.entity_moved do |event|
entity.position = if origin.distance3d(event.entity.position) <= 3.0 if origin.distance3d(event.entity.position) <= 3.0
origin + Vector.up * 2.4 entity.position = origin + Vector.up * 2.4
else else
origin entity.position = origin
end end
end end

View File

@@ -1,3 +0,0 @@
name: "editor"
model: "editor.obj"
collision: "mesh"

View File

@@ -1,32 +0,0 @@
# Blender MTL File: 'editor.blend'
# Material Count: 3
newmtl body
Ns 225.000000
Ka 1.000000 1.000000 1.000000
Kd 0.011935 0.113782 0.401969
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl energy
Ns 225.000000
Ka 1.000000 1.000000 1.000000
Kd 0.000000 0.653036 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl eye
Ns 225.000000
Ka 1.000000 1.000000 1.000000
Kd 0.710554 0.177754 0.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +0,0 @@
name: "islands_terrain"
model: "islands_terrain.obj"
collision: "mesh"

View File

@@ -1,32 +0,0 @@
# Blender MTL File: 'islands_terrain.blend'
# Material Count: 3
newmtl Ground
Ns 225.000000
Ka 1.000000 1.000000 1.000000
Kd 0.137348 0.064835 0.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl Rock
Ns 225.000000
Ka 1.000000 1.000000 1.000000
Kd 0.388300 0.159443 0.000000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2
newmtl Water
Ns 225.000000
Ka 1.000000 1.000000 1.000000
Kd 0.003266 0.332269 0.800000
Ks 0.500000 0.500000 0.500000
Ke 0.000000 0.000000 0.000000
Ni 1.450000
d 1.000000
illum 2

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
component(:building) component(:building)
on.create do |event| on.create do |event|
map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(-1.5, 1.5, -4.52), Vector.new(0, 20, 0), data: { team: nil }) map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(-1.5, 1.5, -4.52), Vector.new(0, 20, 0), data: {team: nil})
map.insert_entity("base", "information_panel", event.entity.position + Vector.new(3, 0, 1), Vector.new(0, -90, 0)) map.insert_entity("base", "information_panel", event.entity.position + Vector.new(3, 0, 1), Vector.new(0, -90, 0))
map.insert_entity("base", "door", event.entity.position + Vector.new(0, 0, 6), Vector.new(0, 0, 0)) map.insert_entity("base", "door", event.entity.position + Vector.new(0, 0, 6), Vector.new(0, 0, 0))
map.insert_entity("base", "door", event.entity.position + Vector.new(0, 0, 6), Vector.new(0, 180, 0)) map.insert_entity("base", "door", event.entity.position + Vector.new(0, 0, 6), Vector.new(0, 180, 0))

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
component(:vehicle) # Generic, Weapon component(:vehicle) # Generic, Weapon
on.button_down(:interact) do |event| on.button_down(:interact) do |event|
CyberarmEngine::Window.instance.console.stdin("#{event.entity.name} handled button_down(:interact)") $window.console.stdin("#{event.entity.name} handled button_down(:interact)")
# if event.player.touching?(event.entity) # if event.player.touching?(event.entity)
# event.player.enter_vehicle # event.player.enter_vehicle
# elsif event.player.driving?(event.entity) or event.player.passenger?(event.entity) # elsif event.player.driving?(event.entity) or event.player.passenger?(event.entity)

View File

@@ -1,22 +0,0 @@
{
"playlists": {
"menus": [
"menu_background"
],
"nighttime": [],
"daytime": []
},
"music": [
{
"name": "menu_background",
"path": "music/untitled-2-revised-extended_mixed.ogg"
}
],
"sounds": [
{
"name": "shield_regen",
"type": "sfx",
"path": "sfx/shield_regen.wav"
}
]
}

View File

@@ -1,13 +1,11 @@
# frozen_string_literal: true
component(:building) component(:building)
on.create do |event| on.create do |event|
map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(6, 1.5, 3), Vector.new(0, -90, 0), data: { team: nil }) map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(6, 1.5, 3), Vector.new(0, -90, 0), data: {team: nil})
map.insert_entity("base", "information_panel", event.entity.position + Vector.new(0.5, 0, 3), Vector.new(0, 90, 0)) map.insert_entity("base", "information_panel", event.entity.position + Vector.new(0.5, 0, 3), Vector.new(0, 90, 0))
map.insert_entity("base", "door", event.entity.position + Vector.new(3.3, 0, 6), Vector.new(0, 0, 0)) map.insert_entity("base", "door", event.entity.position + Vector.new(3.3, 0, 6), Vector.new(0, 0, 0))
map.insert_entity("base", "door", event.entity.position + Vector.new(3.3, 0, 6), Vector.new(0, 180, 0)) map.insert_entity("base", "door", event.entity.position + Vector.new(3.3, 0, 6), Vector.new(0, 180, 0))
# map.insert_particle_emitter(Vector.new(3.0, 15.379, 0.029), Texture.new(path: ["base", "shared", "particles", "smoke", "smoke.png"])) map.insert_particle_emitter(Vector.new(3.0, 15.379, 0.029), Texture.new(path: ["base", "shared", "particles", "smoke", "smoke.png"]))
# map.insert_particle_emitter(Vector.new(5.0, 15.379, 0.029), Texture.new(path: ["base", "shared", "particles", "smoke", "smoke.png"])) map.insert_particle_emitter(Vector.new(5.0, 15.379, 0.029), Texture.new(path: ["base", "shared", "particles", "smoke", "smoke.png"]))
end end

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +0,0 @@
# frozen_string_literal: true
IMICFPS_SERVER_MODE = true
require_relative "i-mic-fps"
director = IMICFPS::Networking::Director.new(mode: :server, hostname: "0.0.0.0", port: 56_789, interface: IMICFPS::Networking::MemoryServer)
director.define_singleton_method(:tick) do |dt|
puts "Ticked: #{dt}"
end
director.run.join

View File

@@ -1,114 +1,127 @@
# frozen_string_literal: true
require "fiddle" require "fiddle"
require "yaml" require "yaml"
require "json" require "json"
require "abbrev" require "abbrev"
require "time" require "time"
require "socket"
require "tmpdir"
require "securerandom"
require "opengl" require "opengl"
require "glu" require "glu"
require "nokogiri" require "nokogiri"
require "i18n"
begin begin
require_relative "../cyberarm_engine/lib/cyberarm_engine" require_relative "../cyberarm_engine/lib/cyberarm_engine"
require_relative "../cyberarm_engine/lib/cyberarm_engine/opengl"
rescue LoadError => e rescue LoadError => e
pp e pp e
require "cyberarm_engine" require "cyberarm_engine"
require "cyberarm_engine/opengl"
end end
Dir.chdir(File.dirname(__FILE__)) Dir.chdir(File.dirname(__FILE__))
require_relative "lib/ext/numeric"
require_relative "lib/ext/load_opengl"
include CyberarmEngine include CyberarmEngine
include OpenGL include OpenGL
include GLU include GLU
def require_all(directory) require_relative "lib/version"
files = Dir["#{directory}/**/*.rb"].sort! require_relative "lib/constants"
file_order = [] require_relative "lib/common_methods"
loop do require_relative "lib/trees/aabb_tree_debug"
failed = [] require_relative "lib/trees/aabb_tree"
first_name_error = nil require_relative "lib/trees/aabb_node"
files.each do |file| require_relative "lib/managers/input_mapper"
begin require_relative "lib/managers/entity_manager"
require_relative file require_relative "lib/managers/light_manager"
file_order << file require_relative "lib/managers/network_manager"
rescue NameError => e require_relative "lib/managers/collision_manager"
failed << file require_relative "lib/managers/physics_manager"
first_name_error ||= e
end
end
if failed.size == files.size require_relative "lib/renderer/renderer"
raise first_name_error require_relative "lib/renderer/g_buffer"
else require_relative "lib/renderer/opengl_renderer"
files = failed require_relative "lib/renderer/bounding_box_renderer"
end
break if failed.empty?
end
# pp file_order.map { |f| f.gsub(".rb", "")} require_relative "lib/states/game_state"
require_relative "lib/ui/menu"
require_relative "lib/ui/command"
require_relative "lib/ui/subcommand"
Dir.glob("#{IMICFPS::GAME_ROOT_PATH}/lib/ui/commands/*.rb").each do |cmd|
require_relative cmd
end end
require_relative "lib/ui/console"
require_relative "lib/ui/menus/main_menu"
require_relative "lib/ui/menus/settings_menu"
require_relative "lib/ui/menus/extras_menu"
require_relative "lib/ui/menus/level_select_menu"
require_relative "lib/ui/menus/game_pause_menu"
require_all "lib" require_relative "lib/states/game_states/game"
require_relative "lib/states/game_states/loading_state"
# Don't launch game if IMICFPS_SERVER_MODE is defined require_relative "lib/subscription"
# or if game is being packaged require_relative "lib/publisher"
def prevent_launch? require_relative "lib/event"
packaging_lockfile = File.expand_path("i-mic-fps-packaging.lock", Dir.tmpdir) require_relative "lib/event_handler"
m = "Game client not launched" require_relative "lib/event_handlers/input"
require_relative "lib/event_handlers/entity_moved"
require_relative "lib/event_handlers/entity_lifecycle"
return [true, "#{m}: Server is running"] if defined?(IMICFPS_SERVER_MODE) && IMICFPS_SERVER_MODE require_relative "lib/scripting"
require_relative "lib/scripting/sandbox"
require_relative "lib/scripting/whitelist"
return [true, "#{m}: Packaging is running"] if defined?(Ocra) require_relative "lib/component"
require_relative "lib/components/building"
if File.exist?(packaging_lockfile) && File.read(packaging_lockfile).strip == IMICFPS::VERSION require_relative "lib/game_objects/entity"
return [true, "#{m}: Packaging lockfile is present (#{packaging_lockfile})"] require_relative "lib/game_objects/light"
require_relative "lib/game_objects/particle_emitter"
require_relative "lib/game_objects/camera"
require_relative "lib/game_objects/entities/player"
require_relative "lib/game_objects/entities/skydome"
require_relative "lib/game_objects/entities/terrain"
require_relative "lib/texture"
require_relative "lib/model"
require_relative "lib/model_cache"
require_relative "lib/model/parser"
require_relative "lib/model/model_object"
require_relative "lib/model/material"
require_relative "lib/model/parsers/wavefront_parser"
require_relative "lib/model/parsers/collada_parser"
require_relative "lib/map_parser"
require_relative "lib/manifest"
require_relative "lib/map"
require_relative "lib/scene"
require_relative "lib/scenes/turn_table"
require_relative "lib/crosshair"
require_relative "lib/demo"
require_relative "lib/window"
require_relative "lib/tools/asset_viewer"
require_relative "lib/tools/map_editor"
if ARGV.join.include?("--profile")
begin
require "ruby-prof"
RubyProf.start
IMICFPS::Window.new.show
result = RubyProf.stop
printer = RubyProf::MultiPrinter.new(result)
printer.print(path: ".", profile: "profile", min_percent: 2)
rescue LoadError
puts "ruby-prof not installed!"
end end
[false, ""]
end
if prevent_launch?[0]
puts prevent_launch?[1]
else else
native = ARGV.join.include?("--native") IMICFPS::Window.new.show
fps_target = ARGV.first.to_i != 0 ? ARGV.first.to_i : 60
window_width = native ? Gosu.screen_width : 1280
window_height = native ? Gosu.screen_height : 720
window_fullscreen = native ? true : false
window = IMICFPS::Window.new(
width: window_width,
height: window_height,
fullscreen: window_fullscreen,
resizable: !window_fullscreen,
update_interval: 1000.0 / fps_target
)
if ARGV.join.include?("--profile")
begin
require "ruby-prof"
RubyProf.start
window.show
result = RubyProf.stop
printer = RubyProf::MultiPrinter.new(result)
printer.print(path: ".", profile: "profile", min_percent: 2)
rescue LoadError
puts "ruby-prof not installed!"
raise
end
else
window.show
end
end end

View File

@@ -1,149 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class CameraController
include CommonMethods
attr_accessor :mode, :camera, :entity, :distance, :origin_distance,
:constant_pitch, :mouse_sensitivity, :mouse_captured
def initialize(camera:, entity: nil, mode: :fpv)
# :fpv - First Person View
# :tpv - Third Person View
@mode = mode
@camera = camera
@entity = entity
@distance = 4
@origin_distance = @distance
@constant_pitch = 20.0
window.mouse_x = window.width / 2
window.mouse_y = window.height / 2
@true_mouse = Point.new(window.width / 2, window.height / 2)
@mouse_sensitivity = 20.0 # Less is faster, more is slower
@mouse_captured = true
@mouse_checked = 0
end
def first_person_view?
@mode == :fpv
end
def distance_from_object
@distance
end
def horizontal_distance_from_object
distance_from_object * Math.cos(@constant_pitch)
end
def vertical_distance_from_object
distance_from_object * Math.sin(@constant_pitch)
end
def position_camera
@distance = if first_person_view?
0
else
@origin_distance
end
x_offset = horizontal_distance_from_object * Math.sin(@entity.orientation.y.degrees_to_radians)
z_offset = horizontal_distance_from_object * Math.cos(@entity.orientation.y.degrees_to_radians)
eye_height = @entity.normalize_bounding_box.max.y
@camera.position.x = @entity.position.x - x_offset
@camera.position.y = @entity.position.y + eye_height
@camera.position.z = @entity.position.z - z_offset
@camera.orientation.y = 180 - @entity.orientation.y
end
def update
position_camera if @entity
return unless @mouse_captured
delta = Float(@true_mouse.x - mouse_x) / (@mouse_sensitivity * @camera.field_of_view) * 70
@camera.orientation.y -= delta
@camera.orientation.y %= 360.0
@camera.orientation.x -= Float(@true_mouse.y - window.mouse_y) / (@mouse_sensitivity * @camera.field_of_view) * 70
@camera.orientation.x = @camera.orientation.x.clamp(-90.0, 90.0)
if @entity
@entity.orientation.y += delta
@entity.orientation.y %= 360.0
end
window.mouse_x = window.width / 2 if window.mouse_x <= 1 || window.mouse_x >= window.width - 1
window.mouse_y = window.height / 2 if window.mouse_y <= 1 || window.mouse_y >= window.height - 1
@true_mouse.x = window.mouse_x
@true_mouse.y = window.mouse_y
end
def button_down(id)
actions = InputMapper.actions(id)
if actions.include?(:release_mouse)
@mouse_captured = false
window.needs_cursor = true
elsif actions.include?(:capture_mouse)
@mouse_captured = true
window.needs_cursor = false
elsif actions.include?(:decrease_view_distance)
@camera.max_view_distance -= 0.5
elsif actions.include?(:increase_view_distance)
@camera.max_view_distance += 0.5
elsif actions.include?(:toggle_first_person_view)
@mode = first_person_view? ? :tpv : :fpv
@entity.visible = !first_person_view? if @entity
elsif actions.include?(:turn_180)
@entity.orientation.y += 180 if @entity
@entity.orientation.y %= 360.0 if @entity
end
end
def button_up(id)
end
def free_move
relative_y_rotation = (@camera.orientation.y + 180)
relative_speed = 2.5
relative_speed = 1.5 if InputMapper.down?(:sneak)
relative_speed = 10.0 if InputMapper.down?(:sprint)
relative_speed *= window.dt
if InputMapper.down?( :forward)
@camera.position.z += Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
@camera.position.x -= Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:backward)
@camera.position.z -= Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
@camera.position.x += Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:strife_left)
@camera.position.z += Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
@camera.position.x += Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:strife_right)
@camera.position.z -= Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
@camera.position.x -= Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:ascend)
@camera.position.y += relative_speed
end
if InputMapper.down?(:descend)
@camera.position.y -= relative_speed
end
end
end
end

View File

@@ -1,38 +1,19 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
def self.assets_path def self.assets_path
File.expand_path("../assets", __dir__) File.expand_path("./../../assets", __FILE__)
end end
module CommonMethods module CommonMethods
def window
CyberarmEngine::Window.instance
end
def delta_time def window; $window; end
(Gosu.milliseconds - window.delta_time) / 1000.0
end
def button_down?(id) def delta_time; (Gosu.milliseconds - window.delta_time) / 1000.0; end
window.button_down?(id) def button_down?(id); window.button_down?(id); end
end
def mouse_x def mouse_x; window.mouse_x; end
window.mouse_x def mouse_y; window.mouse_y; end
end def mouse_x=(int); window.mouse_x = int; end
def mouse_y=(int); window.mouse_y = int; end
def mouse_y
window.mouse_y
end
def mouse_x=(int)
window.mouse_x = int
end
def mouse_y=(int)
window.mouse_y = int
end
def gl(&block) def gl(&block)
window.gl do window.gl do
@@ -43,79 +24,32 @@ class IMICFPS
def formatted_number(number) def formatted_number(number)
string = number.to_s.reverse.scan(/\d{1,3}/).join(",").reverse string = number.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
string.insert(0, "-") if number.negative? string.insert(0, "-") if number < 0
string return string
end end
def control_down? def control_down?; button_down?(Gosu::KbLeftControl) || button_down?(Gosu::KbRightControl); end
button_down?(Gosu::KbLeftControl) || button_down?(Gosu::KbRightControl) def shift_down?; button_down?(Gosu::KbLeftShift) || button_down?(Gosu::KbRightShift); end
end def alt_down?; button_down?(Gosu::KbLeftAlt) || button_down?(Gosu::KbRightAlt); end
def shift_down?
button_down?(Gosu::KbLeftShift) || button_down?(Gosu::KbRightShift)
end
def alt_down?
button_down?(Gosu::KbLeftAlt) || button_down?(Gosu::KbRightAlt)
end
def draw_rect(*args) def draw_rect(*args)
window.draw_rect(*args) window.draw_rect(*args)
end end
def draw_quad(*args) def draw_quad(*args)
window.draw_quad(*args) window.draw_quad(*args)
end end
def fill(color = Gosu::Color::WHITE)
def fill(color = Gosu::Color::WHITE, z = 0) draw_rect(0, 0, window.width, window.height, color)
draw_rect(0, 0, window.width, window.height, color, z)
end end
def fill_quad(x1, y1, x2, y2, x3, y3, x4, y4, color = Gosu::Color::WHITE, z = 0, mode = :default) def gl_error?
draw_quad( e = glGetError()
x1, y1, color, if e != GL_NO_ERROR
x2, y2, color, $stderr.puts "OpenGL error detected by handler at: #{caller[0]}"
x3, y3, color, $stderr.puts " #{gluErrorString(e)} (#{e})\n"
x4, y4, color, exit if window.config.get(:debug_options, :opengl_error_panic)
z, mode
)
end
def menu_background(primary_color, accent_color, color_step, transparency, bar_size, slope)
((Gosu.screen_height + slope) / bar_size).times do |i|
color = Gosu::Color.rgba(
primary_color.red - i * color_step,
primary_color.green - i * color_step,
primary_color.blue - i * color_step,
transparency
)
fill_quad(
0, i * bar_size,
0, slope + (i * bar_size),
window.width / 2, (-slope) + (i * bar_size),
window.width / 2, i * bar_size,
color,
-2
)
fill_quad(
window.width, i * bar_size,
window.width, slope + (i * bar_size),
window.width / 2, (-slope) + (i * bar_size),
window.width / 2, i * bar_size,
color,
-2
)
end end
Gosu.draw_quad(
0, 0, primary_color,
window.width, 0, primary_color,
window.width, window.height, accent_color,
0, window.height, accent_color,
-2
)
end end
end end
end end

View File

@@ -1,27 +1,25 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Component class Component
@components = {} COMPONENTS = {}
def self.get(name) def self.get(name)
@components[name] COMPONENTS.dig(name)
end end
def self.inherited(subclass) def self.inherited(subclass)
@components["__pending"] ||= [] COMPONENTS["__pending"] ||= []
@components["__pending"] << subclass COMPONENTS["__pending"] << subclass
end end
def self.initiate def self.initiate
return unless @components["__pending"] # Already setup return unless COMPONENTS.dig("__pending") # Already setup
@components["__pending"].each do |klass| COMPONENTS["__pending"].each do |klass|
component = klass.new component = klass.new
@components[component.name] = component COMPONENTS[component.name] = component
end end
@components.delete("__pending") COMPONENTS.delete("__pending")
end end
def initialize def initialize
@@ -32,7 +30,9 @@ class IMICFPS
string = self.class.name.split("::").last string = self.class.name.split("::").last
split = string.scan(/[A-Z][a-z]*/) split = string.scan(/[A-Z][a-z]*/)
split.map(&:downcase).join("_").to_s.to_sym component_name = "#{split.map { |s| s.downcase }.join("_")}".to_sym
return component_name
end end
def setup def setup

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Components class Components
class Building < Component class Building < Component

View File

@@ -1,11 +1,10 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
GAME_ROOT_PATH = File.expand_path("..", File.dirname(__FILE__)) GAME_ROOT_PATH = File.expand_path("..", File.dirname(__FILE__))
SANS_FONT = "#{GAME_ROOT_PATH}/static/fonts/Cantarell/Cantarell-Regular.otf" TextureCoordinate = Struct.new(:u, :v, :weight)
BOLD_SANS_FONT = "#{GAME_ROOT_PATH}/static/fonts/Cantarell/Cantarell-Bold.otf" Point = Struct.new(:x, :y)
MONOSPACE_FONT = "#{GAME_ROOT_PATH}/static/fonts/Oxygen_Mono/OxygenMono-Regular.ttf" Color = Struct.new(:red, :green, :blue, :alpha)
Face = Struct.new(:vertices, :uvs, :normals, :colors, :material, :smoothing)
# Objects exported from blender using the default or meter object scale will be close to 1 GL unit # Objects exported from blender using the default or meter object scale will be close to 1 GL unit
MODEL_METER_SCALE = 1.0 MODEL_METER_SCALE = 1.0

View File

@@ -1,35 +1,16 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Crosshair class Crosshair
include CommonMethods include CommonMethods
def initialize(color: Gosu::Color.rgb(255, 127, 0), size: 10, thickness: 3) def initialize(color: Gosu::Color.rgb(255,127,0), size: 10, thickness: 3)
@color = color @color = color
@size = size @size = size
@thickness = thickness @thickness = thickness
end end
def draw def draw
draw_rect( draw_rect(window.width/2-@size, (window.height/2-@size)-@thickness/2, @size*2, @thickness, @color, 0, :default)
window.width / 2 - @size, draw_rect((window.width/2)-@thickness/2, window.height/2-(@size*2), @thickness, @size*2, @color, 0, :default)
(window.height / 2 - @size) - @thickness / 2,
@size * 2,
@thickness,
@color,
0,
:default
)
draw_rect(
(window.width / 2) - @thickness / 2,
window.height / 2 - (@size * 2),
@thickness,
@size * 2,
@color,
0,
:default
)
end end
end end
end end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Demo class Demo
def initialize(camera:, player:, demo:, mode:) def initialize(camera:, player:, demo:, mode:)
@@ -8,7 +6,7 @@ class IMICFPS
@demo = demo @demo = demo
@mode = mode @mode = mode
@index = 0 @index= 0
@tick = 0 @tick = 0
@changed = false @changed = false
@@ -32,7 +30,7 @@ class IMICFPS
@file.puts("tick #{@index}") @file.puts("tick #{@index}")
end end
@file.puts("down #{InputMapper.actions(id)}") @file.puts("down #{InputMapper.action(id)}")
@changed = true @changed = true
end end
end end
@@ -44,7 +42,7 @@ class IMICFPS
@file.puts("tick #{@index}") @file.puts("tick #{@index}")
end end
@file.puts("up #{InputMapper.actions(id)}") @file.puts("up #{InputMapper.action(id)}")
@changed = true @changed = true
end end
end end
@@ -56,37 +54,33 @@ class IMICFPS
@tick += 1 @tick += 1
end end
def playing? def playing?; @mode == :play; end
@mode == :play def recording?; !playing?; end
end
def recording?
!playing?
end
def play def play
if @data[@index]&.start_with?("tick") if @data[@index]&.start_with?("tick")
if @tick == @data[@index].split(" ").last.to_i if @tick == @data[@index].split(" ").last.to_i
@index += 1 @index+=1
until @data[@index]&.start_with?("tick") until(@data[@index]&.start_with?("tick"))
break unless @data[@index] break unless @data[@index]
data = @data[@index].split(" ") data = @data[@index].split(" ")
case data.first if data.first == "up"
when "up"
input = InputMapper.get(data.last.to_sym) input = InputMapper.get(data.last.to_sym)
key = input.is_a?(Array) ? input.first : input key = input.is_a?(Array) ? input.first : input
CyberarmEngine::Window.instance.current_state.button_up(key) if key $window.current_state.button_up(key) if key
when "down" elsif data.first == "down"
input = InputMapper.get(data.last.to_sym) input = InputMapper.get(data.last.to_sym)
key = input.is_a?(Array) ? input.first : input key = input.is_a?(Array) ? input.first : input
CyberarmEngine::Window.instance.current_state.button_down(key) if key $window.current_state.button_down(key) if key
when "mouse" elsif data.first == "mouse"
@camera.orientation.z = data[1].to_f @camera.orientation.z = data[1].to_f
@player.orientation.y = (data[2].to_f * -1) - 180 @player.orientation.y = (data[2].to_f * -1) - 180
else
# hmm
end end
@index += 1 @index += 1

View File

@@ -1,13 +1,9 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class EventHandler class EventHandler
class Event class Event
attr_reader :entity, :context attr_reader :entity, :context
def initialize(entity:, context: nil) def initialize(entity:, context: nil)
@entity = entity @entity, @context = entity, context
@context = context
end end
end end
end end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class EventHandler class EventHandler
@@handlers = {} @@handlers = {}
@@ -24,7 +22,7 @@ class IMICFPS
end end
def self.get(event) def self.get(event)
@@handlers[event] @@handlers.dig(event)
end end
def initialize def initialize

View File

@@ -1,15 +1,12 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class EventHandler class EventHandler
class EntityLifeCycle < EventHandler class EntityLifeCycle < EventHandler
def handles def handles
%i[create move destroy] [:create, :move, :destroy]
end end
def handle(subscriber, context, *args) def handle(subscriber, context, *args)
return unless subscriber.entity == args.first.first return unless subscriber.entity == args.first.first
event = EventHandler::Event.new(entity: subscriber.entity, context: context) event = EventHandler::Event.new(entity: subscriber.entity, context: context)
subscriber.trigger(event) subscriber.trigger(event)

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class EventHandler class EventHandler
class EntityMoved < EventHandler class EntityMoved < EventHandler
@@ -7,7 +5,7 @@ class IMICFPS
[:entity_moved] [:entity_moved]
end end
def handle(subscriber, _context, *args) def handle(subscriber, context, *args)
event = EventHandler::Event.new(entity: args.first.first) event = EventHandler::Event.new(entity: args.first.first)
subscriber.trigger(event) subscriber.trigger(event)

View File

@@ -1,10 +1,8 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class EventHandler class EventHandler
class Input < EventHandler class Input < EventHandler
def handles def handles
%i[button_down button_up] [:button_down, :button_up]
end end
def handle(subscriber, context, *args) def handle(subscriber, context, *args)
@@ -15,8 +13,10 @@ class IMICFPS
if action.is_a?(Numeric) && action == key if action.is_a?(Numeric) && action == key
subscriber.trigger(event) subscriber.trigger(event)
elsif InputMapper.get(action) == key else
subscriber.trigger(event) if InputMapper.get(action) == key
subscriber.trigger(event)
end
end end
end end
end end

View File

@@ -1,13 +0,0 @@
module CyberarmEngine
class Element
alias enter_original enter
def enter(_sender)
if @block && is_a?(CyberarmEngine::Element::Link)
get_sample("#{IMICFPS::GAME_ROOT_PATH}/static/sounds/ui_hover.ogg").play
end
enter_original(_sender)
end
end
end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
case OpenGL.get_platform case OpenGL.get_platform
when :OPENGL_PLATFORM_WINDOWS when :OPENGL_PLATFORM_WINDOWS
OpenGL.load_lib("opengl32.dll", "C:/Windows/System32") OpenGL.load_lib("opengl32.dll", "C:/Windows/System32")
@@ -30,8 +28,8 @@ when :OPENGL_PLATFORM_LINUX
OpenGL.load_lib("libGL.so", gl_library_path) OpenGL.load_lib("libGL.so", gl_library_path)
GLU.load_lib("libGLU.so", gl_library_path) GLU.load_lib("libGLU.so", gl_library_path)
else else
raise "Couldn't find GL libraries" raise RuntimeError, "Couldn't find GL libraries"
end end
else else
raise "Unsupported platform." raise RuntimeError, "Unsupported platform."
end end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
if RUBY_VERSION < "2.5.0" if RUBY_VERSION < "2.5.0"
puts "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-" puts "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
puts "|NOTICE| Ruby is #{RUBY_VERSION} not 2.5.0+..............................|Notice|" puts "|NOTICE| Ruby is #{RUBY_VERSION} not 2.5.0+..............................|Notice|"
@@ -13,7 +11,7 @@ if RUBY_VERSION < "2.5.0"
elsif self > max elsif self > max
max max
else else
self return self
end end
end end
end end

197
lib/game_objects/camera.rb Normal file
View File

@@ -0,0 +1,197 @@
class IMICFPS
class Camera
include CommonMethods
attr_accessor :field_of_view, :mouse_sensitivity
attr_reader :entity, :position, :orientation, :mouse_captured
def initialize(position:, orientation: Vector.new(0, 0, 0), fov: 70.0, min_view_distance: 0.1, max_view_distance: 155.0)
@position = position
@orientation = orientation
@field_of_view = fov
@min_view_distance = min_view_distance
@max_view_distance = max_view_distance
@constant_pitch = 20.0
@entity = nil
@distance = 4
@origin_distance = @distance
self.mouse_x, self.mouse_y = window.width / 2, window.height / 2
@true_mouse = Point.new(window.width / 2, window.height / 2)
@mouse_sensitivity = 20.0 # Less is faster, more is slower
@mouse_captured = true
@mouse_checked = 0
end
def attach_to(entity)
raise "Not an Entity!" unless entity.is_a?(Entity)
@entity = entity
@entity.attach_camera(self)
end
def detach
@entity.detach_camera
@entity = nil
end
def distance_from_object
@distance
end
def horizontal_distance_from_object
distance_from_object * Math.cos(@constant_pitch)
end
def vertical_distance_from_object
distance_from_object * Math.sin(@constant_pitch)
end
def position_camera
if defined?(@entity.first_person_view)
if @entity.first_person_view
@distance = 0
else
@distance = @origin_distance
end
end
x_offset = horizontal_distance_from_object * Math.sin(@entity.orientation.y.degrees_to_radians)
z_offset = horizontal_distance_from_object * Math.cos(@entity.orientation.y.degrees_to_radians)
@position.x = @entity.position.x - x_offset
@position.y = @entity.position.y + 2
@position.z = @entity.position.z - z_offset
@orientation.y = 180 - @entity.orientation.y
end
def draw
#glMatrixMode(matrix) indicates that following [matrix] is going to get used
glMatrixMode(GL_PROJECTION) # The projection matrix is responsible for adding perspective to our scene.
glLoadIdentity # Resets current modelview matrix
# Calculates aspect ratio of the window. Gets perspective view. 45 is degree viewing angle, (0.1, 100) are ranges how deep can we draw into the screen
gluPerspective(@field_of_view, window.width / window.height, 0.1, @max_view_distance)
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
glRotatef(@orientation.x, 1, 0, 0)
glRotatef(@orientation.y, 0, 1, 0)
glTranslatef(-@position.x, -@position.y, -@position.z)
glMatrixMode(GL_MODELVIEW) # The modelview matrix is where object information is stored.
glLoadIdentity
end
def update
if @mouse_captured
delta = Float(@true_mouse.x - self.mouse_x) / (@mouse_sensitivity * @field_of_view) * 70
@orientation.y -= delta
@orientation.y %= 360.0
@orientation.x -= Float(@true_mouse.y - self.mouse_y) / (@mouse_sensitivity * @field_of_view) * 70
@orientation.x = @orientation.x.clamp(-90.0, 90.0)
if @entity
@entity.orientation.y += delta
@entity.orientation.y %= 360.0
position_camera
else
free_move
end
self.mouse_x = window.width / 2 if self.mouse_x <= 1 || window.mouse_x >= window.width-1
self.mouse_y = window.height / 2 if self.mouse_y <= 1 || window.mouse_y >= window.height-1
@true_mouse.x, @true_mouse.y = self.mouse_x, self.mouse_y
end
end
def looking_at
ray = Ray.new(@position, @orientation.direction * -1)
window.current_state.collision_manager.search(ray)
end
def free_move
relative_y_rotation = (@orientation.y + 180)
relative_speed = 2.5
relative_speed = 1.5 if InputMapper.down?(:sneak)
relative_speed = 10.0 if InputMapper.down?(:sprint)
relative_speed *= window.dt
turn_speed = 50.0 * window.dt
if InputMapper.down?( :forward)
@position.z+=Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
@position.x-=Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:backward)
@position.z-=Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
@position.x+=Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:strife_left)
@position.z+=Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
@position.x+=Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:strife_right)
@position.z-=Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
@position.x-=Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
end
if InputMapper.down?(:turn_left)
@orientation.y -= turn_speed
end
if InputMapper.down?(:turn_right)
@orientation.y += turn_speed
end
if InputMapper.down?(:ascend)
@position.y+=relative_speed
end
if InputMapper.down?(:descend)
@position.y-=relative_speed
end
end
def button_up(id)
if InputMapper.is?(:release_mouse, id)
@mouse_captured = false
window.needs_cursor = true
elsif InputMapper.is?(:capture_mouse, id)
@mouse_captured = true
window.needs_cursor = false
elsif InputMapper.is?(:increase_mouse_sensitivity, id)
@mouse_sensitivity+=1
@mouse_sensitivity = @mouse_sensitivity.clamp(1.0, 100.0)
elsif InputMapper.is?(:decrease_mouse_sensitivity, id)
@mouse_sensitivity-=1
@mouse_sensitivity = @mouse_sensitivity.clamp(1.0, 100.0)
elsif InputMapper.is?(:reset_mouse_sensitivity, id)
@mouse_sensitivity = 20.0
elsif InputMapper.is?(:increase_view_distance, id)
# @field_of_view += 1
# @field_of_view = @field_of_view.clamp(1, 100)
@max_view_distance += 1
@max_view_distance = @max_view_distance.clamp(1, 1000)
elsif InputMapper.is?(:decrease_view_distance, id)
# @field_of_view -= 1
# @field_of_view = @field_of_view.clamp(1, 100)
@max_view_distance -= 1
@max_view_distance = @max_view_distance.clamp(1, 1000)
end
end
def aspect_ratio
window.width / window.height.to_f
end
def projection_matrix
Transform.perspective(@field_of_view, aspect_ratio, @min_view_distance, @max_view_distance)
end
def view_matrix
Transform.translate_3d(@position * -1) * Transform.rotate_3d(@orientation)
end
end
end

View File

@@ -1,83 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class Editor < Entity
attr_accessor :speed
attr_reader :bound_model, :first_person_view
def setup
bind_model
@speed = 2.5 # meter's per second
@running_speed = 5.0 # meter's per second
@turn_speed = 50.0
@old_speed = @speed
@mass = 72 # kg
@first_person_view = true
@visible = false
@drag = 0.9
end
def update
super
@position += @velocity * window.dt
@velocity *= @drag
end
def relative_speed
InputMapper.down?(:sprint) ? @running_speed : @speed
end
def relative_y_rotation
@orientation.y * -1
end
def forward
@velocity.z += Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
@velocity.y -= Math.sin(@orientation.x * Math::PI / 180) * relative_speed
@velocity.x -= Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
end
def backward
@velocity.z -= Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
@velocity.y += Math.sin(@orientation.x * Math::PI / 180) * relative_speed
@velocity.x += Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
end
def strife_left
@velocity.z += Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
@velocity.x += Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
end
def strife_right
@velocity.z -= Math.sin(relative_y_rotation * Math::PI / 180) * relative_speed
@velocity.x -= Math.cos(relative_y_rotation * Math::PI / 180) * relative_speed
end
def turn_left
@orientation.y += @turn_speed * delta_time
end
def turn_right
@orientation.y -= @turn_speed * delta_time
end
def ascend
@velocity.y += relative_speed
end
def descend
@velocity.y -= relative_speed
end
def toggle_first_person_view
@first_person_view = !@first_person_view
@visible = !@first_person_view
end
def turn_180
@orientation.y = @orientation.y + 180
@orientation.y %= 360
end
end
end

View File

@@ -1,20 +1,76 @@
# frozen_string_literal: true require "etc"
class IMICFPS class IMICFPS
class Player < Entity class Player < Entity
attr_accessor :speed attr_accessor :speed
attr_reader :name, :bound_model attr_reader :name, :bound_model, :first_person_view
def setup def setup
bind_model bind_model
@speed = 2.5 # meter's per second @speed = 2.5 # meter's per second
@running_speed = 5.0 # meter's per second @running_speed = 5.0 # meter's per second
@turn_speed = 50.0 @turn_speed = 50.0
@old_speed = @speed @old_speed = @speed
@mass = 72 # kg @mass = 72 # kg
@first_person_view = true
@visible = false @visible = false
@drag = 0.6 @drag = 0.6
@devisor = 500.0
@name_image = Gosu::Image.from_text("#{Etc.getlogin}", 100, font: "Consolas", align: :center)
@name_texture_id = Texture.new(image: @name_image).id
end
def draw_nameplate
_width = (@name_image.width / @devisor) / 2
_height = (@name_image.height / @devisor)
_y = 2#normalize_bounding_box(model.bounding_box).max_y+0.05
glPushMatrix
glRotatef(180, 0, 1, 0)
glDisable(GL_LIGHTING)
glEnable(GL_COLOR_MATERIAL)
glEnable(GL_TEXTURE_2D)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND)
glBindTexture(GL_TEXTURE_2D, @name_texture_id)
glBegin(GL_TRIANGLES)
glColor3f(1.0,1.0,1.0)
# TOP LEFT
glTexCoord2f(0, 0)
glVertex3f(0-_width,_y+_height,0)
# TOP RIGHT
glTexCoord2f(1, 0)
glVertex3f(0+_width, _y+_height,0)
# BOTTOM LEFT
glTexCoord2f(0, 1)
glVertex3f(0-_width,_y,0)
# BOTTOM LEFT
glTexCoord2f(0, 1)
glVertex3f(0-_width,_y,0)
# BOTTOM RIGHT
glTexCoord2f(1, 1)
glVertex3f(0+_width, _y,0)
# TOP RIGHT
glTexCoord2f(1, 0)
glVertex3f(0+_width,_y+_height,0)
glEnd
# glDisable(GL_BLEND)
glDisable(GL_TEXTURE_2D)
glEnable(GL_LIGHTING)
glPopMatrix
end
def draw
if !@first_person_view
super
draw_nameplate
end
end end
def update def update
@@ -61,9 +117,19 @@ class IMICFPS
end end
def jump def jump
return unless InputMapper.down?(:jump) && window.director.map.collision_manager.on_ground?(self) if InputMapper.down?(:jump) && window.current_state.map.collision_manager.on_ground?(self)
@velocity.y = 1.5
end
end
@velocity.y = 1.5 def toggle_first_person_view
@first_person_view = !@first_person_view
@visible = !@first_person_view
end
def turn_180
@orientation.y = @orientation.y + 180
@orientation.y %= 360
end end
end end
end end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Skydome < Entity class Skydome < Entity
def setup def setup

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Terrain < Entity class Terrain < Entity
end end

View File

@@ -1,12 +1,13 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
# A game object is any renderable thing # A game object is any renderable thing
class Entity class Entity
include CommonMethods include CommonMethods
attr_accessor :visible, :renderable, :backface_culling, :position, :orientation, :scale, :velocity, :debug_color attr_accessor :visible, :renderable, :backface_culling
attr_reader :name, :bounding_box, :drag, :camera, :manifest, :model attr_accessor :position, :orientation, :scale, :velocity
attr_reader :name, :debug_color, :bounding_box, :drag, :camera, :manifest
def initialize(manifest:, map_entity: nil, spawnpoint: nil, backface_culling: true, run_scripts: true) def initialize(manifest:, map_entity: nil, spawnpoint: nil, backface_culling: true, run_scripts: true)
@manifest = manifest @manifest = manifest
@@ -15,7 +16,7 @@ class IMICFPS
@position = map_entity.position.clone @position = map_entity.position.clone
@orientation = map_entity.orientation.clone @orientation = map_entity.orientation.clone
@scale = map_entity.scale.clone @scale = map_entity.scale.clone
bind_model @bound_model = bind_model
elsif spawnpoint elsif spawnpoint
@position = spawnpoint.position.clone @position = spawnpoint.position.clone
@orientation = spawnpoint.orientation.clone @orientation = spawnpoint.orientation.clone
@@ -44,7 +45,8 @@ class IMICFPS
setup setup
if @model if @bound_model
@bound_model.model.entity = self
@normalized_bounding_box = normalize_bounding_box_with_offset @normalized_bounding_box = normalize_bounding_box_with_offset
normalize_bounding_box normalize_bounding_box
@@ -52,7 +54,7 @@ class IMICFPS
@camera = nil @camera = nil
self return self
end end
def load_scripts def load_scripts
@@ -66,15 +68,22 @@ class IMICFPS
end end
def bind_model def bind_model
model = ModelCache.find_or_cache(manifest: @manifest) model = ModelCache.new(manifest: @manifest)
raise "model isn't a model!" unless model.is_a?(Model)
@model = model raise "model isn't a model!" unless model.is_a?(ModelCache)
@bound_model = model
@bound_model.model.entity = self
@bounding_box = normalize_bounding_box_with_offset @bounding_box = normalize_bounding_box_with_offset
return model
end
def model
@bound_model.model if @bound_model
end end
def unbind_model def unbind_model
@model = nil @bound_model = nil
end end
def attach_camera(camera) def attach_camera(camera)
@@ -92,7 +101,10 @@ class IMICFPS
def draw def draw
end end
def update def update
model.update
unless at_same_position? unless at_same_position?
Publisher.instance.publish(:entity_moved, nil, self) Publisher.instance.publish(:entity_moved, nil, self)
@bounding_box = normalize_bounding_box_with_offset if model @bounding_box = normalize_bounding_box_with_offset if model
@@ -101,16 +113,20 @@ class IMICFPS
@last_position = Vector.new(@position.x, @position.y, @position.z) @last_position = Vector.new(@position.x, @position.y, @position.z)
end end
def debug_color=(color)
@debug_color = color
end
def at_same_position? def at_same_position?
@position == @last_position @position == @last_position
end end
def normalize_bounding_box_with_offset def normalize_bounding_box_with_offset
@model.bounding_box.normalize_with_offset(self) @bound_model.model.bounding_box.normalize_with_offset(self)
end end
def normalize_bounding_box def normalize_bounding_box
@model.bounding_box.normalize(self) @bound_model.model.bounding_box.normalize(self)
end end
def model_matrix def model_matrix

46
lib/game_objects/light.rb Normal file
View File

@@ -0,0 +1,46 @@
class IMICFPS
class Light
DIRECTIONAL = 0
POINT = 1
attr_reader :light_id
attr_accessor :type, :ambient, :diffuse, :specular, :position, :intensity
def initialize(
id:,
type: Light::POINT,
ambient: Vector.new(0.5, 0.5, 0.5),
diffuse: Vector.new(1, 1, 1),
specular: Vector.new(0.2, 0.2, 0.2),
position: Vector.new(0, 0, 0),
intensity: 1
)
@light_id = id
@type = type
@ambient = ambient
@diffuse = diffuse
@specular = specular
@position = position
@intensity = intensity
end
def draw
glLightfv(@light_id, GL_AMBIENT, convert(@ambient).pack("f*"))
glLightfv(@light_id, GL_DIFFUSE, convert(@diffuse, true).pack("f*"))
glLightfv(@light_id, GL_SPECULAR, convert(@specular, true).pack("f*"))
glLightfv(@light_id, GL_POSITION, convert(@position).pack("f*"))
glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, 1)
glEnable(GL_LIGHTING)
glEnable(@light_id)
end
def convert(struct, apply_intensity = false)
if apply_intensity
return struct.to_a.compact.map{ |i| i * @intensity }
else
return struct.to_a.compact
end
end
end
end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
def initialize(position:, image:, interval: 1_500, time_to_live: 3_000, max_particles: 500) def initialize(position:, image:, interval: 1_500, time_to_live: 3_000, max_particles: 500)
end end

View File

@@ -1,44 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
def initialize(player)
@ammo = AmmoWidget.new({ player: player })
@radar = RadarWidget.new({ player: player })
@health = HealthWidget.new({ player: player })
@chat_history = ChatHistoryWidget.new({ player: player })
@score_board = ScoreBoardWidget.new({ player: player })
@squad = SquadWidget.new({ player: player })
@crosshair = CrosshairWidget.new({ player: player })
@chat = ChatWidget.new({ player: player })
@hud_elements = [
@ammo,
@radar,
@health,
@chat_history,
@score_board,
@squad,
@chat,
@crosshair
]
end
def draw
@hud_elements.each(&:draw)
end
def update
@hud_elements.each(&:update)
end
def button_down(id)
@hud_elements.each { |e| e.button_down(id) }
end
def button_up(id)
@hud_elements.each { |e| e.button_up(id) }
end
end
end

View File

@@ -1,76 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class Widget
include CommonMethods
# Widget margin from screen edge
# or how much widget is pushed in
def self.vertical_margin
@@vertical_margin ||= 36
end
def self.vertical_margin=(n)
@@vertical_margin = n
end
def self.horizontal_margin
@@horizontal_margin ||= 10
end
def self.horizontal_margin=(n)
@@horizontal_margin = n
end
# Widget element padding
def self.vertical_padding
@@vertical_padding ||= 10
end
def self.vertical_padding=(n)
@@vertical_padding = n
end
def self.horizontal_padding
@@horizontal_padding ||= 10
end
def self.horizontal_padding=(n)
@@horizontal_padding = n
end
attr_reader :options
def initialize(options = {})
@options = options
@player = options[:player]
setup
end
def setup
end
def draw
end
def update
end
def button_down(id)
end
def button_up(id)
end
def hijack_input!
CyberarmEngine::Window.instance.input_hijack = self
end
def release_input!
CyberarmEngine::Window.instance.input_hijack = nil
end
end
end
end

View File

@@ -1,31 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class AmmoWidget < HUD::Widget
def setup
@text = Text.new("", size: 64, font: MONOSPACE_FONT, border: true, border_color: Gosu::Color::BLACK)
@background = Gosu::Color.new(0x88c64600)
end
def draw
Gosu.draw_rect(
@text.x - Widget.horizontal_padding, @text.y - Widget.vertical_padding,
@text.width + Widget.horizontal_padding * 2, @text.height + Widget.vertical_padding * 2,
@background
)
@text.draw
end
def update
if (Gosu.milliseconds / 1000.0) % 1.0 >= 0.9
random = rand(0..199).to_s.rjust(3, "0")
@text.text = "#{random}/999"
end
@text.x = window.width - (Widget.horizontal_margin + @text.width + Widget.horizontal_padding)
@text.y = window.height - (Widget.vertical_margin + @text.height + Widget.vertical_padding)
end
end
end
end

View File

@@ -1,97 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class ChatWidget < HUD::Widget
def setup
@deliver_to_text = Text.new("", size: 28, font: BOLD_SANS_FONT)
@text = Text.new("", size: 28, font: SANS_FONT)
@text_input = nil
@background = Gosu::Color.new(0x88c64600)
@selection_color = Gosu::Color.new(0x88222222)
@width = @options[:width] || 400
@delivery_options = [:all, :team, :squad]
end
def draw
return unless @text_input
Gosu.draw_rect(
Widget.horizontal_margin, CyberarmEngine::Window.instance.height / 2 - (@text.height / 2 + Widget.horizontal_padding),
@width - Widget.horizontal_padding * 2, @text.height + Widget.vertical_padding * 2,
@background
)
@deliver_to_text.draw
clip_width = @deliver_to_text.width + Widget.horizontal_padding * 3 + Widget.horizontal_margin
Gosu.clip_to(@text.x, @text.y, @width - clip_width, @text.height) do
x = Widget.horizontal_margin + Widget.horizontal_padding + @deliver_to_text.width
cursor_x = x + @text.width(@text_input.text[0...@text_input.caret_pos])
selection_x = x + @text.width(@text_input.text[0...@text_input.selection_start])
selection_width = cursor_x - selection_x
cursor_thickness = 2
Gosu.draw_rect(selection_x, @text.y, selection_width, @text.height, @selection_color)
Gosu.draw_rect(cursor_x, @text.y, cursor_thickness, @text.height, Gosu::Color::WHITE)
@text.draw
end
end
def update
@deliver_to_text.text = "#{@deliver_to}: "
@deliver_to_text.x = Widget.horizontal_margin + Widget.horizontal_padding
@deliver_to_text.y = CyberarmEngine::Window.instance.height / 2 - (@text.height / 2)
@text.text = @text_input&.text.to_s
@text.x = Widget.horizontal_margin + Widget.horizontal_padding + @deliver_to_text.width
@text.y = CyberarmEngine::Window.instance.height / 2 - (@text.height / 2)
end
def button_down(id)
# TODO: Use InputMapper keymap to function
# NOTE: Account for Y in QWERTZ layout
case id
when Gosu::KB_T, Gosu::KB_Y, Gosu::KB_U
return if @text_input
hijack_input!
@text_input = window.text_input = Gosu::TextInput.new
@deliver_to = :all if Gosu.button_down?(Gosu::KbT)
@deliver_to = :team if Gosu.button_down?(Gosu::KbY)
@deliver_to = :squad if Gosu.button_down?(Gosu::KbU)
when Gosu::KB_TAB
return unless @text_input
cycle_deliver_to
end
end
def button_up(id)
return unless @text_input
case id
when Gosu::KB_ENTER, Gosu::KB_RETURN
release_input!
# TODO: Deliver message to server
@text_input = window.text_input = nil
when Gosu::KB_ESCAPE
release_input!
@text_input = window.text_input = nil
end
end
def cycle_deliver_to
i = @delivery_options.index(@deliver_to)
@deliver_to = @delivery_options[(i + 1) % (@delivery_options.size)]
end
end
end
end

View File

@@ -1,75 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class ChatHistoryWidget < HUD::Widget
def setup
@messages = []
@text = CyberarmEngine::Text.new(
"",
size: 16,
x: Widget.horizontal_margin, y: Widget.vertical_margin, z: 45,
border_color: Gosu::Color::BLACK,
font: BOLD_SANS_FONT
)
@last_message_time = 0
@message_interval = 1_500
end
def draw
@text.draw
end
def update
@text.text = @messages.last(15).map { |m| "#{m}\n" }.join
if Gosu.milliseconds - @last_message_time >= @message_interval
@last_message_time = Gosu.milliseconds
@message_interval = rand(500..3_000)
@messages << random_message
end
end
def random_message
usernames = %w[
Cyberarm Cyber TankKiller DavyJones
]
entities = [
"Alternate Tank", "Hover Hank", "Helicopter", "Jeep"
]
locations = [
"Compass Bridge", "Compass Power Plant", "Gort Power Plant", "Gort Bridge", "Nest"
]
events = %i[spot kill target message]
messages = [
"Need more tanks!",
"I need 351 credits to purchase a tank",
"I got 300"
]
segments = {
spot: [
" spotted a <c=ffa51d2d>#{entities.sample}</c> at <c=ff26a269>#{locations.sample}</c>"
],
kill: [
" killed <c=ffa51d2d>#{usernames.sample}</c>"
],
target: [
" targeted <c=ffa51d2d>#{entities.sample} (#{usernames.sample})</c>"
],
message: [
"<c=ffe66100>: #{messages.sample}</c>"
]
}
"<c=ffe66100>#{usernames.sample}</c>#{segments[events.sample].sample}"
end
end
end
end

View File

@@ -1,30 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class CrosshairWidget < HUD::Widget
def setup
@scale = 0.75
@color = Gosu::Color.new(0x44ffffff)
@image = Gosu::Image.new("#{GAME_ROOT_PATH}/static/crosshairs/crosshair.png")
@last_changed_time = Gosu.milliseconds
@change_interval = 1_500
@color = 0xaaffffff
end
def draw
@image.draw(
window.width / 2 - (@image.width * @scale) / 2,
window.height / 2 - (@image.height * @scale) / 2,
46,
@scale,
@scale,
@color
)
end
end
end
end

View File

@@ -1,53 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class HealthWidget < HUD::Widget
def setup
@spacer = 0
@text = Text.new("", font: MONOSPACE_FONT, border: true, border_color: Gosu::Color::BLACK)
@width = 512
@height = 24
@slant = 32
@color = Gosu::Color.new(0x66ffa348)
@shield = Gosu::Color.new(0xaae66100)
@health = 0.0
end
def draw
@text.draw
fill_quad(
window.width / 2 - @width / 2, @spacer + Widget.vertical_margin, # TOP LEFT
window.width / 2 + @width / 2, @spacer + Widget.vertical_margin, # TOP RIGHT
window.width / 2 + @width / 2 - @slant, @spacer + Widget.vertical_margin + @height, # BOTTOM RIGHT
window.width / 2 - @width / 2 + @slant, @spacer + Widget.vertical_margin + @height, # BOTTOM LEFT
@color
)
bottom_right = (window.width / 2 - @width / 2) + @width * @health - @slant
bottom_right = (window.width / 2 - @width / 2) + @slant if @width * @health - @slant < @slant
# Current Health
fill_quad(
window.width / 2 - @width / 2, @spacer + Widget.vertical_margin, # TOP LEFT
(window.width / 2 - @width / 2) + @width * @health, @spacer + Widget.vertical_margin, # TOP RIGHT
bottom_right, @spacer + Widget.vertical_margin + @height, # BOTTOM RIGHT
window.width / 2 - @width / 2 + @slant, @spacer + Widget.vertical_margin + @height, # BOTTOM LEFT
@shield
)
end
def update
percentage = (@health * 100).round.to_s.rjust(3, "0")
@text.text = "[Health #{percentage}%]"
@text.x = window.width / 2 - @text.width / 2
@text.y = @spacer + Widget.vertical_margin + @height / 2 - @text.height / 2
@health += 0.1 * window.dt
@health = 0 if @health > 1.0
end
end
end
end

View File

@@ -1,53 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class RadarWidget < HUD::Widget
def setup
@min_size = 148
@max_size = 288
@target_screen_width = 1920
@size = @max_size
@border_color = Gosu::Color.new(0x88c64600)
@radar_color = Gosu::Color.new(0x88212121)
@text = Text.new("RADAR", size: 18, font: MONOSPACE_FONT, border: true, border_color: Gosu::Color::BLACK)
@image = Gosu::Image.new("#{CYBERARM_ENGINE_ROOT_PATH}/assets/textures/default.png", retro: true)
@scale = (@size - Widget.horizontal_padding * 2.0) / @image.width
end
def draw
Gosu.draw_rect(
Widget.horizontal_margin, window.height - (@size + Widget.vertical_margin),
@size, @size,
@border_color
)
Gosu.draw_rect(
Widget.horizontal_margin + Widget.horizontal_padding,
window.height - (@size + Widget.vertical_margin) + Widget.vertical_padding,
@size - Widget.horizontal_padding * 2, @size - Widget.horizontal_padding * 2,
@radar_color
)
@image.draw(
Widget.horizontal_margin + Widget.horizontal_padding,
window.height - (@size + Widget.vertical_margin) + Widget.vertical_padding,
46, @scale, @scale, 0x88ffffff
)
@text.draw
end
def update
@size = (window.width / @target_screen_width.to_f * @max_size).clamp(@min_size, @max_size)
@scale = (@size - Widget.horizontal_padding * 2.0) / @image.width
@text.text = "X: #{@player.position.x.round(1)} Y: #{@player.position.y.round(1)} Z: #{@player.position.z.round(1)}"
@text.x = Widget.horizontal_margin + @size / 2 - @text.width / 2
@text.y = window.height - (Widget.vertical_margin + @size + @text.height)
end
end
end
end

View File

@@ -1,94 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class ScoreBoardWidget < HUD::Widget
def setup
@usernames = Array("AAAA".."zzzz") # "Adran".."Zebra")
@text = CyberarmEngine::Text.new(
"",
size: 16,
x: Widget.horizontal_margin, y: Widget.vertical_margin, z: 45,
border: true,
border_color: Gosu::Color::BLACK,
font: BOLD_SANS_FONT
)
set_text
end
def draw
@text.draw
end
def update
@text.x = window.width - (@text.markup_width + Widget.horizontal_margin)
end
def generate_random_data
number_of_players = rand(2..32)
data = {
teams: [
{
name: "Compass",
credits: 0,
score: 0
},
{
name: "Gort",
credits: 0,
score: 0
}
],
players: []
}
number_of_players.times do |i|
data[:players] << {
team: i.even? ? 0 : 1,
username: @usernames.sample,
score: rand(0..29_999),
credits: rand(0..9_999)
}
end
data[:teams][0][:credits] = data[:players].select { |player| (player[:team]).zero? }.map { |player| player[:credits] }.reduce(0, :+)
data[:teams][0][:score] = data[:players].select { |player| (player[:team]).zero? }.map { |player| player[:score] }.reduce(0, :+)
data[:teams][1][:credits] = data[:players].select { |player| player[:team] == 1 }.map { |player| player[:credits] }.reduce(0, :+)
data[:teams][1][:score] = data[:players].select { |player| player[:team] == 1 }.map { |player| player[:score] }.reduce(0, :+)
data[:teams] = data[:teams].sort_by { |team| team[:score] }.reverse
data[:players] = data[:players].sort_by { |player| player[:score] }.reverse
data
end
def set_text
team_header = %i[name credits score]
player_header = %i[username credits score]
data = generate_random_data
text = ""
text += "# Team Credits Score\n"
data[:teams].each_with_index do |team, i|
i += 1
text += "<c=#{team[:name] == 'Compass' ? 'ffe66100' : 'ffa51d2d'}>#{i} #{team[:name]} #{i.even? ? team[:credits] : '-----'} #{team[:score]}</c>\n"
end
text += "\n"
text += "# Name Credits Score\n"
data[:players].each_with_index do |player, i|
i += 1
text += "<c=#{player[:team].even? ? 'ffe66100' : 'ffa51d2d'}>#{i} #{player[:username]} #{player[:team].even? ? player[:credits] : '-----'} #{player[:score]}</c>\n"
end
@text.text = text
end
end
end
end

View File

@@ -1,36 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class HUD
class SquadWidget < HUD::Widget
def setup
@min_size = 148
@max_size = 288 # RADAR size
@target_screen_width = 1920
@size = @max_size
@color = Gosu::Color.new(0xff00aa00)
@text = Text.new(
"MATE\nTinyTanker\nOther Player Dude\nHuman 0xdeadbeef",
size: 18,
font: SANS_FONT,
color: @color,
border: true,
border_color: Gosu::Color::BLACK,
)
end
def draw
@text.draw
end
def update
@size = (window.width / @target_screen_width.to_f * @max_size).clamp(@min_size, @max_size)
@text.x = Widget.horizontal_margin + @size + Widget.horizontal_padding
@text.y = window.height - (Widget.vertical_margin + @text.height)
end
end
end
end

View File

@@ -1,9 +1,6 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class CollisionManager class CollisionManager
attr_reader :map, :collisions attr_reader :map, :collisions
def initialize(map:) def initialize(map:)
@map = map @map = map
@collisions = {} @collisions = {}
@@ -50,13 +47,13 @@ class IMICFPS
next if entity.manifest.collision_resolution == :static # Only dynamic entities can be resolved next if entity.manifest.collision_resolution == :static # Only dynamic entities can be resolved
search = @aabb_tree.search(entity.bounding_box) search = @aabb_tree.search(entity.bounding_box)
if search.size.positive? if search.size > 0
search.reject! { |ent| ent == entity || !ent.collidable? } search.reject! {|ent| ent == entity || !ent.collidable?}
broadphase[entity] = search broadphase[entity] = search
end end
end end
broadphase.each do |_entity, _collisions| broadphase.each do |entity, _collisions|
_collisions.each do |ent| _collisions.each do |ent|
# aabb vs aabb # aabb vs aabb
# next unless entity.bounding_box.intersect?(ent.bounding_box) # next unless entity.bounding_box.intersect?(ent.bounding_box)
@@ -75,15 +72,15 @@ class IMICFPS
# AABBTree on entities is relative to model origin of 0,0,0 # AABBTree on entities is relative to model origin of 0,0,0
def localize_entity_bounding_box(entity, target) def localize_entity_bounding_box(entity, target)
return entity.bounding_box if target.position.zero? && target.orientation.zero? return entity.bounding_box if target.position == 0 && target.orientation == 0
# "tranform" entity bounding box into target's space # "tranform" entity bounding box into target's space
local = target.position # needs tweaking, works well enough for now local = (target.position) # needs tweaking, works well enough for now
box = entity.bounding_box.clone box = entity.bounding_box.clone
box.min -= local box.min -= local
box.max -= local box.max -= local
box return box
end end
def on_ground?(entity) # TODO: Use some form of caching to speed this up def on_ground?(entity) # TODO: Use some form of caching to speed this up
@@ -98,7 +95,7 @@ class IMICFPS
broadphase.detect do |ent| broadphase.detect do |ent|
ray = Ray.new(entity.position - ent.position, Vector.down) ray = Ray.new(entity.position - ent.position, Vector.down)
if ent.model.aabb_tree.search(ray).size.positive? if ent.model.aabb_tree.search(ray).size > 0
on_ground = true on_ground = true
return true return true
end end
@@ -107,7 +104,7 @@ class IMICFPS
break if on_ground break if on_ground
end end
on_ground return on_ground
end end
end end
end end

View File

@@ -1,34 +1,31 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
module EntityManager # Get included into GameState context module EntityManager # Get included into GameState context
def add_entity(entity) def add_entity(entity)
if @collision_manager && entity.manifest.collision @collision_manager.add(entity) if @collision_manager && entity.manifest.collision# Add every entity to collision manager
@collision_manager.add(entity)
end # Add every entity to collision manager
Publisher.instance.publish(:create, nil, entity) Publisher.instance.publish(:create, nil, entity)
@entities << entity @entities << entity
end end
def insert_entity(package, name, position, orientation, _data = {}) def insert_entity(package, name, position, orientation, data = {})
ent = MapParser::Entity.new(package, name, position, orientation, Vector.new(1, 1, 1)) ent = MapParser::Entity.new(package, name, position, orientation, Vector.new(1,1,1))
add_entity(IMICFPS::Entity.new(map_entity: ent, manifest: Manifest.new(package: package, name: name))) add_entity(IMICFPS::Entity.new(map_entity: ent, manifest: Manifest.new(package: package, name: name)))
end end
def find_entity(entity) def find_entity(entity)
@entities.detect { |e| e == entity } @entities.detect {|entity| entity == entity}
end end
def find_entity_by(name:) def find_entity_by(name:)
@entities.detect { |entity| entity.name == name } @entities.detect { |entity| entity.name == name}
end end
def remove_entity(entity) def remove_entity(entity)
return unless (ent = @entities.detect { |e| e == entity }) ent = @entities.detect {|entity| entity == entity}
if ent
@collision_manager.remove(entity) if @collision_manager && entity.manifest.collision @collision_manager.remove(entity) if @collision_manager && entity.manifest.collision
@publisher.publish(:destroy, nil, entity) @publisher.publish(:destroy, nil, entity)
@entities.delete(ent) @entities.delete(ent)
end
end end
def entities def entities

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class InputMapper class InputMapper
@@keymap = {} @@keymap = {}
@@ -17,12 +15,11 @@ class IMICFPS
if id_or_action.is_a?(Integer) if id_or_action.is_a?(Integer)
@@keys[id_or_action] = true @@keys[id_or_action] = true
else else
query = @@keymap[id_or_action] query = @@keymap.dig(id_or_action)
case query if query.is_a?(Integer)
when Integer query
query elsif query.is_a?(Array)
when Array
query.each do |key| query.each do |key|
@@keys[key] = true @@keys[key] = true
end end
@@ -36,12 +33,11 @@ class IMICFPS
if id_or_action.is_a?(Integer) if id_or_action.is_a?(Integer)
@@keys[id_or_action] = false @@keys[id_or_action] = false
else else
query = @@keymap[id_or_action] query = @@keymap.dig(id_or_action)
case query if query.is_a?(Integer)
when Integer
query query
when Array elsif query.is_a?(Array)
query.each do |key| query.each do |key|
@@keys[key] = false @@keys[key] = false
end end
@@ -52,14 +48,12 @@ class IMICFPS
end end
def self.get(action) def self.get(action)
@@keymap[action] @@keymap.dig(action)
end end
def self.set(action, key) def self.set(action, key)
raise "action must be a symbol" unless action.is_a?(Symbol) raise "action must be a symbol" unless action.is_a?(Symbol)
unless key.is_a?(Integer) || key.is_a?(Array) raise "key must be a whole number or Array of whole numbers, got #{key}" unless key.is_a?(Integer) || key.is_a?(Array)
raise "key must be a whole number or Array of whole numbers, got #{key}"
end
warn "InputMapper.set(:#{action}) is already defined as #{@@keymap[action]}" if @@keymap[action] warn "InputMapper.set(:#{action}) is already defined as #{@@keymap[action]}" if @@keymap[action]
@@ -79,7 +73,7 @@ class IMICFPS
end end
def self.is?(action, query_key) def self.is?(action, query_key)
keys = @@keymap[action] keys = @@keymap.dig(action)
if keys.is_a?(Array) if keys.is_a?(Array)
keys.include?(query_key) keys.include?(query_key)
@@ -88,19 +82,23 @@ class IMICFPS
end end
end end
def self.actions(key) def self.action(key)
@@keymap.select do |action, value| answer = nil
case value @@keymap.detect do |action, value|
when Array if value.is_a?(Array)
action if value.include?(key) answer = action if value.include?(key)
when key else
action if value == key
answer = action
end
end end
end.map { |keymap| keymap.first.is_a?(Symbol) ? keymap.first : keymap.first.first } end
answer
end end
def self.reset_keys def self.reset_keys
@@keys.each do |key, _value| @@keys.each do |key, value|
@@keys[key] = false @@keys[key] = false
end end
end end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
module LightManager module LightManager
MAX_LIGHTS = OpenGL::GL_MAX_LIGHTS MAX_LIGHTS = OpenGL::GL_MAX_LIGHTS
@@ -8,7 +6,7 @@ class IMICFPS
@lights << model @lights << model
end end
def find_light def find_light()
end end
def lights def lights
@@ -16,7 +14,7 @@ class IMICFPS
end end
def light_count def light_count
@lights.count + 1 @lights.count+1
end end
def clear_lights def clear_lights
@@ -25,8 +23,7 @@ class IMICFPS
def available_light def available_light
raise "Using to many lights, #{light_count}/#{LightManager::MAX_LIGHTS}" if light_count > LightManager::MAX_LIGHTS raise "Using to many lights, #{light_count}/#{LightManager::MAX_LIGHTS}" if light_count > LightManager::MAX_LIGHTS
puts "OpenGL::GL_LIGHT#{light_count}" if $window.config.get(:debug_options, :stats)
puts "OpenGL::GL_LIGHT#{light_count}" if CyberarmEngine::Window.instance.config.get(:debug_options, :stats)
Object.const_get "OpenGL::GL_LIGHT#{light_count}" Object.const_get "OpenGL::GL_LIGHT#{light_count}"
end end
end end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class PhysicsManager class PhysicsManager
def initialize(collision_manager:) def initialize(collision_manager:)
@@ -17,7 +15,7 @@ class IMICFPS
end end
def resolve(entity, other) def resolve(entity, other)
entity.velocity.y = other.velocity.y if other.velocity.y < entity.velocity.y && entity.velocity.y.negative? entity.velocity.y = other.velocity.y if other.velocity.y < entity.velocity.y && entity.velocity.y < 0
end end
def simulate def simulate
@@ -34,7 +32,7 @@ class IMICFPS
entity.velocity.y = 0 entity.velocity.y = 0
else else
entity.velocity.y -= @collision_manager.map.gravity * entity.delta_time if entity.manifest.physics entity.velocity.y -= @collision_manager.map.gravity * entity.delta_time if entity.manifest.physics
entity.velocity.y = 0 if entity.velocity.y.negative? entity.velocity.y = 0 if entity.velocity.y < 0
end end
end end
end end

View File

@@ -1,111 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module SoundManager
extend CyberarmEngine::Common
@masters = {}
@effects = []
@playlists = {}
@current_playlist = nil
@current_playlist_package = nil
@current_playlist_name = nil
@current_playlist_index = 0
def self.master_volume
window.config.get(:options, :audio, :volume_master)
end
def self.music_volume
window.config.get(:options, :audio, :volume_music) * master_volume
end
def self.sfx_volume
window.config.get(:options, :audio, :volume_sound_effects) * master_volume
end
def self.load_master(package)
return if @masters[package]
hash = JSON.parse(File.read("#{IMICFPS.assets_path}/#{package}/shared/sound/master.json"))
@masters[package] = hash
end
def self.sound(package, name)
raise "Missing sound: '#{name}' in package '#{package}'" unless (data = sound_data(package, name.to_s))
get_sample("#{IMICFPS.assets_path}/#{package}/shared/sound/#{data['path']}")
end
def self.sound_data(package, name)
load_master(package)
if (master = @masters[package])
return master["sounds"].find { |s| s["name"] == name }
end
nil
end
def self.sound_effect(klass, options)
@effects << klass.new(options)
end
def self.music(package, name)
raise "Missing song: '#{name}' in package '#{package}'" unless (data = music_data(package, name.to_s))
get_song("#{IMICFPS.assets_path}/#{package}/shared/sound/#{data['path']}")
end
def self.music_data(package, name)
load_master(package)
if (master = @masters[package])
return master["music"].find { |s| s["name"] == name }
end
nil
end
def self.playlist_data(package, name)
load_master(package)
if (master = @masters[package])
return master.dig("playlists", name.to_s)
end
nil
end
def self.play_playlist(package, name)
return if @current_playlist_name == name.to_s
return unless (list = playlist_data(package, name.to_s))
@current_playlist = list
@current_playlist_package = package
@current_playlist_name = name.to_s
@current_playlist_index = 0
@current_song = music(@current_playlist_package, @current_playlist[@current_playlist_index])
@current_song.volume = music_volume
@current_song.play
end
def self.update
@effects.each { |e| e.update; @effects.delete(e) if e.done? }
return unless @current_playlist
if !@current_song&.playing? && music_volume > 0.0
@current_playlist_index += 1
@current_playlist_index = 0 if @current_playlist_index >= @current_playlist.size
@current_song = music(@current_playlist_package, @current_playlist[@current_playlist_index])
@current_song.play
end
@current_song&.volume = music_volume
@current_song&.stop if music_volume < 0.1
end
end
end

View File

@@ -1,35 +1,33 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Manifest class Manifest
attr_reader :name, :model, :collision, :collision_mesh, :collision_resolution, :physics, :scripts, :uses attr_reader :name, :model, :collision, :collision_mesh, :collision_resolution, :physics, :scripts, :uses
def initialize(manifest_file: nil, package: nil, name: nil) def initialize(manifest_file: nil, package: nil, name: nil)
unless manifest_file unless manifest_file
raise "Entity package not specified!" unless package raise "Entity package not specified!" unless package
raise "Entity name not specified!" unless name raise "Entity name not specified!" unless name
manifest_file = "#{IMICFPS.assets_path}/#{package}/#{name}/manifest.yaml" manifest_file = "#{IMICFPS.assets_path}/#{package}/#{name}/manifest.yaml"
end end
raise "No manifest found at: #{manifest_file}" unless File.exist?(manifest_file) raise "No manifest found at: #{manifest_file}" unless File.exist?(manifest_file)
@file = manifest_file @file = manifest_file
parse(manifest_file) parse(manifest_file)
end end
def parse(file) def parse(file)
data = YAML.safe_load(File.read(file)) data = YAML.load(File.read(file))
# required # required
@name = data["name"] @name = data["name"]
@model = data["model"] @model = data["model"]
# optional # optional
@collision = data["collision"] || nil @collision = data["collision"] ? data["collision"] : nil
@collision_mesh = data["collision_mesh"] || nil @collision_mesh = data["collision_mesh"] ? data["collision_mesh"] : nil
@collision_resolution = data["collision_resolution"] ? data["collision_resolution"].to_sym : :static @collision_resolution = data["collision_resolution"] ? data["collision_resolution"].to_sym : :static
@physics = data["physics"] || false @physics = data["physics"] ? data["physics"] : false
@scripts = data["scripts"] ? parse_scripts(data["scripts"]) : [] @scripts = data["scripts"] ? parse_scripts(data["scripts"]) : []
@uses = data["uses"] ? parse_dependencies(data["uses"]) : [] # List of entities that this Entity uses @uses = data["uses"] ? parse_dependencies(data["uses"]) : [] # List of entities that this Entity uses
end end
@@ -41,7 +39,7 @@ class IMICFPS
if script.start_with?("!") if script.start_with?("!")
script = script.sub("!", "") script = script.sub("!", "")
path = "#{File.expand_path('../shared/', file_path)}/scripts/#{script}" path = File.expand_path("../shared/", file_path) + "/scripts/" + script
else else
path = "#{file_path}/scripts/#{script}" path = "#{file_path}/scripts/#{script}"
end end
@@ -49,7 +47,7 @@ class IMICFPS
list << Script.new(script, File.read("#{path}.rb")) list << Script.new(script, File.read("#{path}.rb"))
end end
list return list
end end
def parse_dependencies(list) def parse_dependencies(list)
@@ -58,7 +56,7 @@ class IMICFPS
dependencies << Dependency.new(item["package"], item["name"]) dependencies << Dependency.new(item["package"], item["name"])
end end
dependencies return dependencies
end end
def file_path def file_path

View File

@@ -1,13 +1,11 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Map class Map
include EntityManager include EntityManager
include LightManager include LightManager
include CommonMethods include CommonMethods
attr_reader :collision_manager, :gravity attr_reader :collision_manager
attr_reader :gravity
def initialize(map_parser:, gravity: IMICFPS::EARTH_GRAVITY) def initialize(map_parser:, gravity: IMICFPS::EARTH_GRAVITY)
@map_parser = map_parser @map_parser = map_parser
@gravity = gravity @gravity = gravity
@@ -16,97 +14,27 @@ class IMICFPS
@lights = [] @lights = []
@collision_manager = CollisionManager.new(map: self) @collision_manager = CollisionManager.new(map: self)
@renderer = window.renderer
Publisher.new Publisher.new
end end
def setup def setup
add_terrain if @map_parser.terrain.name add_entity(Terrain.new(map_entity: @map_parser.terrain, manifest: Manifest.new(package: @map_parser.terrain.package, name: @map_parser.terrain.name)))
add_skybox if @map_parser.skydome.name
add_lights
add_entities
# TODO: Add player entity from director add_entity(Skydome.new(map_entity: @map_parser.skydome, manifest: Manifest.new(package: @map_parser.skydome.package, name: @map_parser.skydome.name), backface_culling: false))
add_entity(
Player.new(
spawnpoint: @map_parser.spawnpoints.sample,
manifest: Manifest.new(
package: "base",
name: "character"
)
)
)
end
def add_terrain
add_entity(
Terrain.new(
map_entity: @map_parser.terrain,
manifest: Manifest.new(
package: @map_parser.terrain.package,
name: @map_parser.terrain.name
)
)
)
end
def add_skybox
add_entity(
Skydome.new(
map_entity: @map_parser.skydome,
backface_culling: false,
manifest: Manifest.new(
package: @map_parser.skydome.package,
name: @map_parser.skydome.name
)
)
)
end
def add_lights
@map_parser.lights.each do |l| @map_parser.lights.each do |l|
add_light( add_light(Light.new(id: available_light, position: l.position, diffuse: l.diffuse, ambient: l.ambient, specular: l.specular, intensity: l.intensity))
Light.new(
id: available_light,
type: l.type,
position: l.position,
diffuse: l.diffuse,
ambient: l.ambient,
specular: l.specular,
intensity: l.intensity
)
)
end end
# Default lights if non are defined
return unless @map_parser.lights.size.zero?
add_light(
Light.new(
id: available_light,
position: Vector.new(30, 10.0, 30)
)
)
add_light(
Light.new(
id: available_light,
position: Vector.new(0, 100, 0), diffuse: Color.new(1.0, 0.5, 0.1)
)
)
end
def add_entities
@map_parser.entities.each do |ent| @map_parser.entities.each do |ent|
add_entity( add_entity(Entity.new(map_entity: ent, manifest: Manifest.new(package: ent.package, name: ent.name)))
Entity.new(
map_entity: ent,
manifest: Manifest.new(
package: ent.package,
name: ent.name
)
)
)
end end
add_entity(Player.new(spawnpoint: @map_parser.spawnpoints.sample, manifest: Manifest.new(package: "base", name: "character")))
# add_light(Light.new(id: available_light, position: Vector.new(30, 10.0, 30)))
# add_light(Light.new(id: available_light, position: Vector.new(0, 100, 0), diffuse: Color.new(1.0, 0.5, 0.1)))
end end
def data def data
@@ -118,12 +46,12 @@ class IMICFPS
Gosu.gl do Gosu.gl do
gl_error? gl_error?
glClearColor(0, 0.2, 0.5, 1) # skyish blue glClearColor(0,0.2,0.5,1) # skyish blue
gl_error? gl_error?
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen and the depth buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen and the depth buffer
gl_error? gl_error?
window.renderer.draw(camera, @lights, @entities) @renderer.draw(camera, @lights, @entities)
end end
end end
@@ -131,6 +59,7 @@ class IMICFPS
@collision_manager.update @collision_manager.update
@entities.each(&:update) @entities.each(&:update)
# @lights.each(&:update)
end end
end end
end end

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class MapParser class MapParser
attr_reader :metadata, :terrain, :skydome, :lights, :entities, :spawnpoints, :assets, :missing_assets attr_reader :metadata, :terrain, :skydome, :lights, :entities, :spawnpoints
attr_reader :assets, :missing_assets
def initialize(map_file:) def initialize(map_file:)
@metadata = MapParser::MetaData.new @metadata = MapParser::MetaData.new
@terrain = MapParser::Entity.new @terrain = MapParser::Entity.new
@@ -18,17 +16,6 @@ class IMICFPS
parse(map_file) parse(map_file)
end end
def light_type(type)
case type.downcase.strip
when "directional"
CyberarmEngine::Light::DIRECTIONAL
when "spot"
CyberarmEngine::Light::SPOT
else
CyberarmEngine::Light::POINT
end
end
def parse(file) def parse(file)
data = JSON.parse(File.read(file)) data = JSON.parse(File.read(file))
@@ -40,7 +27,7 @@ class IMICFPS
@metadata.thumbnail = section["thumbnail"] # TODO: convert thumbnail to Image @metadata.thumbnail = section["thumbnail"] # TODO: convert thumbnail to Image
@metadata.description = section["description"] @metadata.description = section["description"]
else else
warn "Map metadata is missing!" raise "Map metadata is missing!"
end end
if section = data["terrain"] if section = data["terrain"]
@@ -64,7 +51,7 @@ class IMICFPS
end end
@terrain.water_level = section["water_level"] @terrain.water_level = section["water_level"]
else else
warn "Map terrain data is missing!" raise "Map terrain data is missing!"
end end
if section = data["skydome"] if section = data["skydome"]
@@ -87,13 +74,13 @@ class IMICFPS
@skydome.scale = Vector.new(1, 1, 1) @skydome.scale = Vector.new(1, 1, 1)
end end
else else
warn "Map skydome data is missing!" raise "Map skydome data is missing!"
end end
if section = data["lights"] if section = data["lights"]
section.each do |l| section.each do |l|
light = MapParser::Light.new light = MapParser::Light.new
light.type = light_type(l["type"]) light.type = IMICFPS::Light::POINT # TODO: fix me
light.position = Vector.new( light.position = Vector.new(
l["position"]["x"], l["position"]["x"],
l["position"]["y"], l["position"]["y"],
@@ -150,7 +137,7 @@ class IMICFPS
@entities << entity @entities << entity
end end
else else
warn "Map has no entities!" raise "Map has no entities!"
end end
if section = data["spawnpoints"] if section = data["spawnpoints"]
@@ -171,7 +158,7 @@ class IMICFPS
@spawnpoints << spawnpoint @spawnpoints << spawnpoint
end end
else else
warn "Map has no spawnpoints!" raise "Map has no spawnpoints!"
end end
end end

250
lib/model.rb Normal file
View File

@@ -0,0 +1,250 @@
class IMICFPS
class Model
include CommonMethods
attr_accessor :objects, :materials, :vertices, :uvs, :texures, :normals, :faces, :colors, :bones
attr_accessor :scale, :entity, :material_file, :current_material, :current_object, :vertex_count, :smoothing
attr_reader :position, :bounding_box, :textured_material, :file_path
attr_reader :positions_buffer_id, :colors_buffer_id, :normals_buffer_id, :uvs_buffer_id, :textures_buffer_id
attr_reader :vertex_array_id
attr_reader :aabb_tree
def initialize(file_path:, entity: nil)
@file_path = file_path
@entity = entity
update if @entity
@material_file = nil
@current_object = nil
@current_material=nil
@vertex_count = 0
@objects = []
@materials= {}
@vertices = []
@colors = []
@uvs = []
@normals = []
@faces = []
@bones = []
@smoothing= 0
@bounding_box = BoundingBox.new
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
type = File.basename(file_path).split(".").last.to_sym
parser = Model::Parser.find(type)
unless parser
raise "Unsupported model type '.#{type}', supported models are: #{Model::Parser.supported_formats}"
end
parse(parser)
puts "#{@file_path.split('/').last} took #{((Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)-start_time)/1000.0).round(2)} seconds to parse" if window.config.get(:debug_options, :stats)
@has_texture = false
@materials.each do |key, material|
if material.texture_id
@has_texture = true
@textured_material = key
end
end
allocate_gl_objects
populate_vertex_buffer
configure_vao
@objects.each {|o| @vertex_count+=o.vertices.size}
@objects.each_with_index do |o, i|
puts " Model::Object Name: #{o.name}, Vertices: #{o.vertices.size}" if window.config.get(:debug_options, :stats)
end
window.number_of_vertices+=@vertex_count
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
# build_collision_tree
puts " Building mesh collision tree took #{((Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)-start_time)/1000.0).round(2)} seconds" if window.config.get(:debug_options, :stats)
end
def parse(parser)
parser.new(self).parse
end
def calculate_bounding_box(vertices, bounding_box)
unless bounding_box.min.x.is_a?(Float)
vertex = vertices.last
bounding_box.min.x = vertex.x
bounding_box.min.y = vertex.y
bounding_box.min.z = vertex.z
bounding_box.max.x = vertex.x
bounding_box.max.y = vertex.y
bounding_box.max.z = vertex.z
end
vertices.each do |vertex|
bounding_box.min.x = vertex.x if vertex.x <= bounding_box.min.x
bounding_box.min.y = vertex.y if vertex.y <= bounding_box.min.y
bounding_box.min.z = vertex.z if vertex.z <= bounding_box.min.z
bounding_box.max.x = vertex.x if vertex.x >= bounding_box.max.x
bounding_box.max.y = vertex.y if vertex.y >= bounding_box.max.y
bounding_box.max.z = vertex.z if vertex.z >= bounding_box.max.z
end
end
def allocate_gl_objects
# Allocate arrays for future use
@vertex_array_id = nil
buffer = " " * 4
glGenVertexArrays(1, buffer)
@vertex_array_id = buffer.unpack('L2').first
# Allocate buffers for future use
@positions_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@positions_buffer_id = buffer.unpack('L2').first
@colors_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@colors_buffer_id = buffer.unpack('L2').first
@normals_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@normals_buffer_id = buffer.unpack('L2').first
@uvs_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@uvs_buffer_id = buffer.unpack('L2').first
@textures_buffer_id = nil
buffer = " " * 4
glGenBuffers(1, buffer)
@textures_buffer_id = buffer.unpack('L2').first
end
def populate_vertex_buffer
pos = []
colors = []
norms = []
uvs = []
tex_ids = []
@faces.each do |face|
pos << face.vertices.map { |vert| [vert.x, vert.y, vert.z] }
colors << face.colors.map { |color| [color.red, color.green, color.blue] }
norms << face.normals.map { |vert| [vert.x, vert.y, vert.z, vert.weight] }
if has_texture?
uvs << face.uvs.map { |vert| [vert.x, vert.y, vert.z] }
tex_ids << face.material.texture_id ? face.material.texture_id.to_f : -1.0
end
end
glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id)
glBufferData(GL_ARRAY_BUFFER, pos.flatten.size * Fiddle::SIZEOF_FLOAT, pos.flatten.pack("f*"), GL_STATIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, @colors_buffer_id)
glBufferData(GL_ARRAY_BUFFER, colors.flatten.size * Fiddle::SIZEOF_FLOAT, colors.flatten.pack("f*"), GL_STATIC_DRAW)
glBindBuffer(GL_ARRAY_BUFFER, @normals_buffer_id)
glBufferData(GL_ARRAY_BUFFER, norms.flatten.size * Fiddle::SIZEOF_FLOAT, norms.flatten.pack("f*"), GL_STATIC_DRAW)
if has_texture?
glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id)
glBufferData(GL_ARRAY_BUFFER, uvs.flatten.size * Fiddle::SIZEOF_FLOAT, uvs.flatten.pack("f*"), GL_STATIC_DRAW)
# glBindBuffer(GL_ARRAY_BUFFER, @textures_buffer_id)
# glBufferData(GL_ARRAY_BUFFER, tex_ids.flatten.size * Fiddle::SIZEOF_FLOAT, tex_ids.flatten.pack("f*"), GL_STATIC_DRAW)
end
glBindBuffer(GL_ARRAY_BUFFER, 0)
end
def configure_vao
glBindVertexArray(@vertex_array_id)
gl_error?
# index, size, type, normalized, stride, pointer
# vertices (positions)
glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id)
gl_error?
# inPosition
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
# colors
glBindBuffer(GL_ARRAY_BUFFER, @colors_buffer_id)
# inColor
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
# normals
glBindBuffer(GL_ARRAY_BUFFER, @normals_buffer_id)
# inNormal
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
if has_texture?
# uvs
glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id)
# inUV
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
# texture ids
glBindBuffer(GL_ARRAY_BUFFER, @textures_buffer_id)
# inTextureID
glVertexAttribPointer(4, 1, GL_FLOAT, GL_FALSE, 0, nil)
gl_error?
end
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
end
def build_collision_tree
@aabb_tree = AABBTree.new
@faces.each do |face|
box = BoundingBox.new
box.min = face.vertices.first.dup
box.max = face.vertices.first.dup
face.vertices.each do |vertex|
if vertex.sum < box.min.sum
box.min = vertex.dup
elsif vertex.sum > box.max.sum
box.max = vertex.dup
end
end
# FIXME: Handle negatives
box.min *= 1.5
box.max *= 1.5
@aabb_tree.insert(face, box)
end
puts @aabb_tree.inspect if window.config.get(:debug_options, :stats)
end
def update
@position = @entity.position
@scale = @entity.scale
end
def has_texture?
@has_texture
end
def release_gl_resources
if @vertex_array_id
end
end
end
end

20
lib/model/material.rb Normal file
View File

@@ -0,0 +1,20 @@
class IMICFPS
class Model
class Material
attr_accessor :name, :ambient, :diffuse, :specular
attr_reader :texture_id
def initialize(name)
@name = name
@ambient = Color.new(1, 1, 1, 1)
@diffuse = Color.new(1, 1, 1, 1)
@specular= Color.new(1, 1, 1, 1)
@texture = nil
@texture_id = nil
end
def set_texture(texture_path)
@texture_id = Texture.new(path: texture_path).id
end
end
end
end

123
lib/model/model_object.rb Normal file
View File

@@ -0,0 +1,123 @@
class IMICFPS
class Model
class ModelObject
attr_reader :id, :name, :vertices, :textures, :normals, :bounding_box, :debug_color
attr_accessor :faces, :scale
def initialize(id, name)
@id = id
@name = name
@vertices = []
@textures = []
@normals = []
@faces = []
@bounding_box = BoundingBox.new
@debug_color = Color.new(1.0,1.0,1.0)
@scale = 1.0
# Faces array packs everything:
# vertex = index[0]
# uv = index[1]
# normal = index[2]
# material = index[3]
end
def reflatten
@vertices_list = nil
@textures_list = nil
@normals_list = nil
flattened_vertices
flattened_textures
flattened_normals
end
def flattened_vertices
unless @vertices_list
@debug_color = @faces.first.material.diffuse
list = []
@faces.each do |face|
face.vertices.each do |v|
next unless v
list << v.x*@scale
list << v.y*@scale
list << v.z*@scale
list << v.weight
end
end
@vertices_list_size = list.size
@vertices_list = list.pack("f*")
end
return @vertices_list
end
def flattened_vertices_size
@vertices_list_size
end
def flattened_textures
unless @textures_list
list = []
@faces.each do |face|
face.uvs.each do |v|
next unless v
list << v.x
list << v.y
list << v.z
end
end
@textures_list_size = list.size
@textures_list = list.pack("f*")
end
return @textures_list
end
def flattened_normals
unless @normals_list
list = []
@faces.each do |face|
face.normals.each do |n|
next unless n
list << n.x
list << n.y
list << n.z
end
end
@normals_list_size = list.size
@normals_list = list.pack("f*")
end
return @normals_list
end
def flattened_materials
unless @materials_list
list = []
@faces.each do |face|
material = face.material
next unless material
face.vertices.each do # Add material to each vertex
list << material.diffuse.red
list << material.diffuse.green
list << material.diffuse.blue
# list << material.alpha
end
end
@materials_list_size = list.size
@materials_list = list.pack("f*")
end
return @materials_list
end
end
end
end

69
lib/model/parser.rb Normal file
View File

@@ -0,0 +1,69 @@
class IMICFPS
class Model
class Parser
@@parsers = []
def self.handles
raise NotImplementedError, "Model::Parser#handles must return an array of file extensions that this parser supports"
end
def self.inherited(parser)
@@parsers << parser
end
def self.find(file_type)
found_parser = @@parsers.find do |parser|
parser.handles.include?(file_type)
end
return found_parser
end
def self.supported_formats
@@parsers.map { |parser| parser.handles }.flatten.map { |s| ".#{s}" }.join(", ")
end
def initialize(model)
@model = model
end
def parse
end
def set_object(id: nil, name: nil)
_model = nil
if id
_model = @model.objects.find { |o| o.id == id }
elsif name
_model = @model.objects.find { |o| o.name == name }
else
raise "Must provide either an id: or name:"
end
if _model
@model.current_object = _model
else
raise "Couldn't find ModelObject!"
end
end
def change_object(id, name)
@model.objects << Model::ModelObject.new(id, name)
@model.current_object = @model.objects.last
end
def set_material(name)
@model.current_material = name
end
def add_material(name, material)
@model.materials[name] = material
end
def current_material
@model.materials[@model.current_material]
end
end
end
end

View File

@@ -0,0 +1,119 @@
class IMICFPS
class ColladaParser < Model::Parser
def self.handles
[:dae]
end
def parse
@collada = Nokogiri::XML(File.read(@model.file_path))
@collada.css("library_materials material").each do |material|
parse_material(material)
end
@collada.css("library_geometries geometry").each do |geometry|
parse_geometry(geometry)
end
@model.calculate_bounding_box(@model.vertices, @model.bounding_box)
@model.objects.each do |o|
@model.calculate_bounding_box(o.vertices, o.bounding_box)
end
end
def parse_material(material)
name = material.attributes["id"].value
effect_id = material.at_css("instance_effect").attributes["url"].value
mat = Model::Material.new(name)
effect = @collada.at_css("[id=\"#{effect_id.sub('#', '')}\"]")
emission = effect.at_css("emission color")
diffuse = effect.at_css("diffuse color").children.first.to_s.split(" ").map { |c| Float(c) }
mat.diffuse = Color.new(*diffuse[0..2])
add_material(name, mat)
end
def parse_geometry(geometry)
geometry_id = geometry.attributes["id"].value
geometry_name = geometry.attributes["name"].value
change_object(geometry_id, geometry_name)
mesh = geometry.at_css("mesh")
get_positions(geometry_id, mesh)
get_normals(geometry_id, mesh)
get_texture_coordinates(geometry_id, mesh)
build_faces(geometry_id, mesh)
end
def get_positions(id, mesh)
positions = mesh.at_css("[id=\"#{id}-positions\"]")
array = positions.at_css("[id=\"#{id}-positions-array\"]")
stride = Integer(positions.at_css("[source=\"##{id}-positions-array\"]").attributes["stride"].value)
list = array.children.first.to_s.split(" ").map{ |f| Float(f) }.each_slice(stride).each do |slice|
position = Vector.new(*slice)
@model.current_object.vertices << position
@model.vertices << position
end
end
def get_normals(id, mesh)
normals = mesh.at_css("[id=\"#{id}-normals\"]")
array = normals.at_css("[id=\"#{id}-normals-array\"]")
stride = Integer(normals.at_css("[source=\"##{id}-normals-array\"]").attributes["stride"].value)
list = array.children.first.to_s.split(" ").map{ |f| Float(f) }.each_slice(stride).each do |slice|
normal = Vector.new(*slice)
@model.current_object.normals << normal
@model.normals << normal
end
end
def get_texture_coordinates(id, mesh)
end
def build_faces(id, mesh)
material_name = mesh.at_css("triangles").attributes["material"].value
set_material(material_name)
positions_index = []
normals_index = []
uvs_index = []
mesh.at_css("triangles p").children.first.to_s.split(" ").map { |i| Integer(i) }.each_slice(3).each do |slice|
positions_index << slice[0]
normals_index << slice[1]
uvs_index << slice[2]
end
norm_index = 0
positions_index.each_slice(3) do |slice|
face = Face.new
face.vertices = []
face.uvs = []
face.normals = []
face.colors = []
face.material = current_material
face.smoothing= @model.smoothing
slice.each do |index|
face.vertices << @model.vertices[index]
# face.uvs << @model.uvs[index]
face.normals << @model.normals[normals_index[norm_index]]
face.colors << current_material.diffuse
norm_index += 1
end
@model.current_object.faces << face
@model.faces << face
end
end
end
end

View File

@@ -0,0 +1,157 @@
class IMICFPS
class WavefrontParser < Model::Parser
def self.handles
[:obj]
end
def parse
lines = 0
list = File.read(@model.file_path).split("\n")
list.each do |line|
lines+=1
line = line.strip
array = line.split(' ')
case array[0]
when 'mtllib'
@model.material_file = array[1]
parse_mtllib
when 'usemtl'
set_material(array[1])
when 'o'
change_object(nil, array[1])
when 's'
set_smoothing(array[1])
when 'v'
add_vertex(array)
when 'vt'
add_texture_coordinate(array)
when 'vn'
add_normal(array)
when 'f'
verts = []
uvs = []
norms = []
array[1..3].each do |f|
verts << f.split("/")[0]
uvs << f.split("/")[1]
norms << f.split("/")[2]
end
face = Face.new
face.vertices = []
face.uvs = []
face.normals = []
face.colors = []
face.material = current_material
face.smoothing= @model.smoothing
mat = face.material.diffuse
color = mat
verts.each_with_index do |v, index|
if uvs.first != ""
face.vertices << @model.vertices[Integer(v)-1]
face.uvs << @model.uvs[Integer(uvs[index])-1]
face.normals << @model.normals[Integer(norms[index])-1]
face.colors << color
else
face.vertices << @model.vertices[Integer(v)-1]
face.uvs << nil
face.normals << @model.normals[Integer(norms[index])-1]
face.colors << color
end
end
@model.current_object.faces << face
@model.faces << face
end
end
puts "Total Lines: #{lines}" if $window.config.get(:debug_options, :stats)
@model.calculate_bounding_box(@model.vertices, @model.bounding_box)
@model.objects.each do |o|
@model.calculate_bounding_box(o.vertices, o.bounding_box)
end
end
def parse_mtllib
file = File.open(@model.file_path.sub(File.basename(@model.file_path), '')+@model.material_file, 'r')
file.readlines.each do |line|
array = line.strip.split(' ')
case array.first
when 'newmtl'
material = Model::Material.new(array.last)
@model.current_material = array.last
@model.materials[array.last] = material
when 'Ns' # Specular Exponent
when 'Ka' # Ambient color
@model.materials[@model.current_material].ambient = Color.new(Float(array[1]), Float(array[2]), Float(array[3]))
when 'Kd' # Diffuse color
@model.materials[@model.current_material].diffuse = Color.new(Float(array[1]), Float(array[2]), Float(array[3]))
when 'Ks' # Specular color
@model.materials[@model.current_material].specular = Color.new(Float(array[1]), Float(array[2]), Float(array[3]))
when 'Ke' # Emissive
when 'Ni' # Unknown (Blender Specific?)
when 'd' # Dissolved (Transparency)
when 'illum' # Illumination model
when 'map_Kd' # Diffuse texture
texture = File.basename(array[1])
texture_path = "#{File.expand_path("../../", @model.file_path)}/textures/#{texture}"
@model.materials[@model.current_material].set_texture(texture_path)
end
end
end
def set_smoothing(value)
if value == "1"
@model.smoothing = true
else
@model.smoothing = false
end
end
def add_vertex(array)
@model.vertex_count+=1
vert = nil
if array.size == 5
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), Float(array[4]))
elsif array.size == 4
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), 1.0)
else
raise
end
@model.current_object.vertices << vert
@model.vertices << vert
end
def add_normal(array)
vert = nil
if array.size == 5
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), Float(array[4]))
elsif array.size == 4
vert = Vector.new(Float(array[1]), Float(array[2]), Float(array[3]), 1.0)
else
raise
end
@model.current_object.normals << vert
@model.normals << vert
end
def add_texture_coordinate(array)
texture = nil
if array.size == 4
texture = Vector.new(Float(array[1]), 1-Float(array[2]), Float(array[3]))
elsif array.size == 3
texture = Vector.new(Float(array[1]), 1-Float(array[2]), 1.0)
else
raise
end
@model.current_object.textures << texture
@model.uvs << texture
end
end
end

43
lib/model_cache.rb Normal file
View File

@@ -0,0 +1,43 @@
class IMICFPS
class ModelCache
CACHE = {}
attr_reader :model, :name, :debug_color
def initialize(manifest:, entity: nil)
@name = manifest.name
@model_file = model_file = manifest.file_path + "/model/#{manifest.model}"
@type = File.basename(@model_file).split(".").last.to_sym
@debug_color = Color.new(0.0, 1.0, 0.0)
@model = nil
unless load_model_from_cache
@model = IMICFPS::Model.new(file_path: @model_file)
cache_model
end
return self
end
def load_model_from_cache
found = false
if CACHE[@type].is_a?(Hash)
if CACHE[@type][@model_file]
@model = CACHE[@type][@model_file]#.dup # Don't know why, but adding .dup improves performance with Sponza (1 fps -> 20 fps)
puts "Used cached model for: #{@model_file.split('/').last}" if $window.config.get(:debug_options, :stats)
found = true
end
end
return found
end
def cache_model
CACHE[@type] = {} unless CACHE[@type].is_a?(Hash)
CACHE[@type][@model_file] = @model
end
end
end

View File

@@ -1,34 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
MULTICAST_ADDRESS = "224.0.0.1"
MULTICAST_PORT = 30_000
REMOTE_GAMEHUB = "i-mic.cyberarm.dev"
REMOTE_GAMEHUB_PORT = 98_765
DEFAULT_SERVER_HOSTNAME = "0.0.0.0"
DEFAULT_SERVER_PORT = 56_789
DEFAULT_SERVER_QUERY_PORT = 28_900
RESERVED_PEER_ID = 0
DEFAULT_PEER_LIMIT = 32
HARD_PEER_LIMIT = 254
def self.milliseconds
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
end
# https://github.com/jpignata/blog/blob/master/articles/multicast-in-ruby.md
def self.broadcast_lan_lobby
socket = UDPSocket.open
socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
socket.send("IMICFPS_LAN_LOBBY", 0, MULTICAST_ADDRESS, MULTICAST_PORT)
socket.close
end
def self.handle_lan_multicast
end
end
end

View File

@@ -1,41 +0,0 @@
# I-MIC FPS / CyberarmEngine Networking System
End Goal: Reliable and ordered packets of abitrary size
Current Goal: Unreliable, unordered packets of limited size
## Internal Packet Format
Based on [minetest's network protocol](https://dev.minetest.net/Network_Protocol)
### Base header
All packet headers start with these fields
```
Protocol Version: Unsigned char
Packet Type: Unsigned char
Peer ID: Unsigned char
```
### Basic packet header
No unique header fields
### Control packet header
```
Control Type: Unsigned char
Control Data: Unsigned 16-bit integer
```
Control Types:
* ACK - Acknowledge receipt of reliable packet
* SET_PEER_ID - Set peer id of connected client, client must provide this to continue communicating
* PING - Used to track peer's network latency
* HEARTBEAT - Used as keep alive
* DISCONNECT - Peer is disconnecting
### Split Packet Header
```
Sequence Number: Unsigned 16-bit number
Chunk Count: Unsigned 16-bit number
Chunk Number: Unsigned 16-bit number
```
### Reliable Packet Header
```
Sequence Number: Unsigned 16-bit number
```

View File

@@ -1,10 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class Channel
def initialize(id:, mode:)
end
end
end
end

View File

@@ -1,82 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class Connection
attr_reader :hostname, :port, :peer
def initialize(hostname:, port:, channels: 3)
@hostname = hostname
@port = port
@channels = Array(0..channels).map { |id| Channel.new(id: id, mode: :default) }
@peer = Peer.new(id: 0, hostname: "", port: "")
end
# Callbacks #
def connected
end
def disconnected(reason:)
end
def reconnected
end
def packet_received(message:, channel:)
end
# Functions #
def send_packet(message:, reliable: false, channel: 0)
@peer.write_queue << PacketHandler.create_raw_packet(peer: @peer, message: message, reliable: reliable, channel: channel)
end
def connect(timeout: Protocol::TIMEOUT_PERIOD)
@socket = UDPSocket.new
write(packet: PacketHandler.create_control_packet(peer: @peer, control_type: Protocol::CONTROL_CONNECT))
end
def disconnect(timeout: Protocol::TIMEOUT_PERIOD)
end
def update
while read
end
@peer.write_queue.reverse.each do |packet|
write(packet: packet)
@peer.write_queue.delete(packet)
end
if Networking.milliseconds - @peer.last_write_time > Protocol::HEARTBEAT_INTERVAL
@peer.write_queue << PacketHandler.create_control_packet(peer: @peer, control_type: Protocol::CONTROL_HEARTBEAT)
end
end
def read
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
pkt = PacketHandler.handle(host: self, raw: data, peer: @peer)
packet_received(message: pkt.message, channel: -1) if pkt.is_a?(RawPacket)
@peer.total_packets_received += 1
@peer.total_data_received += data.length
@peer.last_read_time = Networking.milliseconds
true
rescue IO::WaitReadable
false
end
def write(packet:)
raw = packet.encode
@socket.send(raw, 0, @hostname, @port)
@peer.total_packets_sent += 1
@peer.total_data_sent += raw.length
@peer.last_write_time = Networking.milliseconds
end
end
end
end

View File

@@ -1,32 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class Packet
attr_reader :protocol_version, :peer_id, :channel, :message
def self.decode(raw)
header = raw.unpack(CyberarmEngine::Networking::Protocol::PACKET_BASE_HEADER)
Packet.new(protocol_version: header[0], peer_id: header[1], channel: header[2], message: raw[Protocol::PACKET_BASE_HEADER_LENGTH...raw.length])
end
def initialize(protocol_version:, peer_id:, channel:, message:)
@protocol_version = protocol_version
@peer_id = peer_id
@channel = channel
@message = message
end
def encode
header = [
@protocol_version,
@peer_id,
@channel
].pack(CyberarmEngine::Networking::Protocol::PACKET_BASE_HEADER)
"#{header}#{@message}"
end
end
end
end

View File

@@ -1,140 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
module PacketHandler
def self.type_to_name(type:)
Protocol.constants.select { |const| const.to_s.start_with?("PACKET_") }
.find { |const| Protocol.const_get(const) == type }
end
def self.handle(host:, raw:, peer:)
packet = Packet.decode(raw)
type = packet.message.unpack1("C")
puts "#{host.class} received #{type_to_name(type: type)}"
pp raw
case type
when Protocol::PACKET_CONTROL
handle_control_packet(host, packet, peer)
when Protocol::PACKET_RAW
handle_raw_packet(packet)
when Protocol::PACKET_RELIABLE
handle_reliable_packet(host, packet, peer)
else
raise NotImplementedError, "A Packet handler for #{type_to_name(type: type)}[#{type}] is not implemented!"
end
end
def self.handle_control_packet(host, packet, peer)
pkt = ControlPacket.decode(packet.message)
case pkt.control_type
when Protocol::CONTROL_CONNECT # TOSERVER only
if (peer_id = host.available_peer_id)
peer.id = peer_id
host.peers << peer
peer.write_queue << create_control_packet(peer: peer, control_type: Protocol::CONTROL_SET_PEER_ID, message: [peer_id].pack("n"))
host.peer_connected(peer: peer)
else
host.write(
peer: peer,
packet: PacketHandler.create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_DISCONNECT,
message: "ERROR: max number of clients already connected"
)
)
end
when Protocol::CONTROL_SET_PEER_ID # TOCLIENT only
peer.id = pkt.message.unpack1("n")
host.connected
when Protocol::CONTROL_DISCONNECT
if host.is_a?(Server)
host.peer_disconnected(peer: peer)
else
host.disconnected(reason: pkt.message)
end
when Protocol::CONTROL_HEARTBEAT
when Protocol::CONTROL_PING
peer.write_queue << PacketHandler.create_control_packet(
peer: peer, control_type: Protocol::CONTROL_PONG,
reliable: true,
message: [Networking.milliseconds].pack("Q") # Uint64, native endian
)
when Protocol::CONTROL_PONG
sent_time = pkt.message.unpack1("Q")
difference = Networking.milliseconds - sent_time
peer.ping = difference
end
nil
end
def self.handle_raw_packet(packet)
RawPacket.decode(packet.message)
end
def self.handle_reliable_packet(host, packet, peer)
# TODO: Preserve delivery order of reliable packets
pkt = ReliablePacket.decode(packet.message)
peer.write_queue << create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_ACKNOWLEDGE,
message: [pkt.sequence_number].pack("n")
)
handle(host: host, raw: pkt.message, peer: peer)
end
def self.create_control_packet(peer:, control_type:, message: nil, reliable: false, channel: 0)
message_packet = nil
if reliable
warn "Reliable packets are not yet implemented!"
packet = Packet.new(
protocol_version: Protocol::PROTOCOL_VERSION,
peer_id: peer.id,
channel: channel,
message: ControlPacket.new(control_type: control_type, message: message).encode
)
message_packet = ReliablePacket.new(sequence_number: peer.next_reliable_sequence_number, message: packet.encode)
else
message_packet = ControlPacket.new(control_type: control_type, message: message)
end
Packet.new(
protocol_version: Protocol::PROTOCOL_VERSION,
peer_id: peer.id,
channel: channel,
message: message_packet.encode
)
end
def self.create_raw_packet(peer:, message:, reliable: false, channel: 0)
message_packet = nil
if reliable
warn "Reliable packets are not yet implemented!"
packet = RawPacket.new(message: message)
message_packet = ReliablePacket.new(sequence_number: peer.next_reliable_sequence_number, message: packet.encode)
else
message_packet = RawPacket.new(message: message)
end
Packet.new(
protocol_version: Protocol::PROTOCOL_VERSION,
peer_id: peer.id,
channel: channel,
message: message_packet.encode
)
end
end
end
end

View File

@@ -1,34 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class ControlPacket
attr_reader :message, :type, :control_type
HEADER_PACKER = "CC"
HEADER_LENGTH = 1 + 1 # bytes
def self.decode(raw_message)
header = raw_message.unpack(HEADER_PACKER)
message = raw_message[HEADER_LENGTH..raw_message.length - 1]
ControlPacket.new(type: header[0], control_type: header[1], message: message)
end
def initialize(control_type:, message: nil, type: Protocol::PACKET_CONTROL)
@type = type
@control_type = control_type
@message = message
end
def encode
header = [
@type,
@control_type
].pack(HEADER_PACKER)
"#{header}#{@message}"
end
end
end
end

View File

@@ -1,32 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class RawPacket
attr_reader :message, :type
HEADER_PACKER = "C"
HEADER_LENGTH = 1 # bytes
def self.decode(raw_message)
header = raw_message.unpack(HEADER_PACKER)
message = raw_message[HEADER_LENGTH..raw_message.length - 1]
RawPacket.new(type: header[0], message: message)
end
def initialize(message:, type: Protocol::PACKET_RAW)
@type = type
@message = message
end
def encode
header = [
@type
].pack(HEADER_PACKER)
"#{header}#{@message}"
end
end
end
end

View File

@@ -1,34 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class ReliablePacket
attr_reader :message, :type, :sequence_number
HEADER_PACKER = "Cn"
HEADER_LENGTH = 1 + 2 # bytes
def self.decode(raw_message)
header = raw_message.unpack(HEADER_PACKER)
message = raw_message[HEADER_LENGTH..raw_message.length - 1]
ReliablePacket.new(type: header[0], sequence_number: header[1], message: message)
end
def initialize(sequence_number:, message:, type: Protocol::PACKET_RELIABLE)
@type = type
@sequence_number = sequence_number
@message = message
end
def encode
header = [
@type,
@sequence_number
].pack(HEADER_PACKER)
"#{header}#{@message}"
end
end
end
end

View File

@@ -1,45 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class Peer
attr_reader :id, :hostname, :port, :data, :read_queue, :write_queue
attr_accessor :total_packets_sent, :total_packets_received,
:total_data_sent, :total_data_received,
:last_read_time, :last_write_time,
:ping
def initialize(id:, hostname:, port:)
@id = id
@hostname = hostname
@port = port
@data = {}
@read_queue = []
@write_queue = []
@last_read_time = Networking.milliseconds
@last_write_time = Networking.milliseconds
@total_packets_sent = 0
@total_packets_received = 0
@total_data_sent = 0
@total_data_received = 0
@ping = 0
@reliable_sequence_number = 65_500
end
def id=(n)
raise "Peer id must be an integer" unless n.is_a?(Integer)
@id = n
end
def next_reliable_sequence_number
@reliable_sequence_number = (@reliable_sequence_number + 1) % 65_535
end
end
end
end

View File

@@ -1,31 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
module Protocol
MAX_PACKET_SIZE = 1024 # bytes
PROTOCOL_VERSION = 0 # u32
HEARTBEAT_INTERVAL = 5_000 # ms
TIMEOUT_PERIOD = 30_000 # ms
PACKET_BASE_HEADER = "NnC" # protocol version (u32), sender peer id (u16), channel (u8)
PACKET_BASE_HEADER_LENGTH = 4 + 2 + 1 # bytes
# protocol packets
PACKET_RELIABLE = 0
PACKET_FRAGMENT = 1
PACKET_CONTROL = 2
PACKET_RAW = 3
# control packet types
CONTROL_CONNECT = 30
CONTROL_SET_PEER_ID = 31
CONTROL_DISCONNECT = 32
CONTROL_ACKNOWLEDGE = 33
CONTROL_HEARTBEAT = 34
CONTROL_PING = 35
CONTROL_PONG = 36
CONTROL_SET_PEER_MTU = 37 # In future
end
end
end

View File

@@ -1,186 +0,0 @@
# frozen_string_literal: true
module CyberarmEngine
module Networking
class Server
attr_reader :hostname, :port, :max_peers, :peers
attr_accessor :total_packets_sent, :total_packets_received,
:total_data_sent, :total_data_received,
:last_read_time, :last_write_time
def initialize(
hostname: CyberarmEngine::Networking::DEFAULT_SERVER_HOSTNAME,
port: CyberarmEngine::Networking::DEFAULT_SERVER_PORT,
max_peers: CyberarmEngine::Networking::DEFAULT_PEER_LIMIT,
channels: 3
)
@hostname = hostname
@port = port
@max_peers = max_peers + 2
@channels = Array(0..channels).map { |id| Channel.new(id: id, mode: :default) }
@peers = []
@last_read_time = Networking.milliseconds
@last_write_time = Networking.milliseconds
@total_packets_sent = 0
@total_packets_received = 0
@total_data_sent = 0
@total_data_received = 0
end
# Callbacks #
# Called when peer connects
def peer_connected(peer:)
end
# Called when peer times out or explicitly disconnects
def peer_disconnected(peer:, reason:)
end
### REMOVE? ###
# Called when peer was not sending heartbeats or regular packets for a
# period of time, but was not logically disconnected and removed, and started
# send packets again.
#
# TLDR: peer was temporarily unreachable but did not timeout.
def peer_reconnected(peer:)
end
# Called when a (logical) packet is received from peer
def packet_received(peer:, message:, channel:)
end
# Functions #
# Bind server
def bind
# TODO: Handle socket errors
@socket = UDPSocket.new
@socket.bind(@hostname, @port)
end
# Send packet to specified peer
def send_packet(peer:, message:, reliable: false, channel: 0)
if (peer = @peers[peer])
packet = PacketHandler.create_raw_packet(message, reliable, channel)
peer.write_queue << packet
else
# TODO: Handle no such peer error
end
end
# Send packet to all connected peer
def broadcast_packet(message:, reliable: false, channel: 0)
@peers.each { |peer| send_packet(peer: peer.id, message: message, reliable: reliable, channel: channel) }
end
# Disconnect peer
def disconnect_peer(peer:, reason: "")
if (peer = @peers[peer])
packet = PacketHandler.create_disconnect_packet(peer.id, reason)
peer.write_now!(packet)
@peers.delete(peer)
end
end
def update
while read
end
# handle write queue
# TODO: handle reliable packets differently
@peers.each do |peer|
if Networking.milliseconds - peer.last_read_time > Protocol::TIMEOUT_PERIOD
message = "ERROR: connection timed out"
write(
peer: peer,
packet: PacketHandler.create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_DISCONNECT,
message: message
)
)
peer_disconnected(peer: peer, reason: message)
@peers.delete(peer)
next
end
if Networking.milliseconds - peer.last_write_time > Protocol::HEARTBEAT_INTERVAL
write(
peer: peer,
packet: PacketHandler.create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_PING
)
)
end
while (packet = peer.write_queue.shift)
write(peer: peer, packet: packet)
end
end
end
# !--- this following functions are meant for internal use only ---! #
def available_peer_id
peer_ids = @peers.map(&:id)
ids = (2..@max_peers).to_a - peer_ids
ids.size.positive? ? ids.first : nil
end
def read
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
peer = nil
if (peer = @peers.find { |pr| pr.hostname == addr[2] && pr.port == addr[1] })
pkt = PacketHandler.handle(host: self, raw: data, peer: peer)
packet_received(peer: peer, message: pkt.message, channel: 0) if pkt.is_a?(RawPacket)
else
peer = Peer.new(id: 0, hostname: addr[2], port: addr[1])
pkt = PacketHandler.handle(host: self, raw: data, peer: peer)
if pkt && !pkt.is_a?(ControlPacket) && pkt.control_type != Protocol::CONTROL_CONNECT
write(
peer: peer,
packet: PacketHandler.create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_DISCONNECT,
message: "ERROR: peer not connected"
)
)
end
end
@total_packets_received += 1
@total_data_received += data.length
@last_read_time = Networking.milliseconds
peer.total_packets_received += 1
peer.total_data_received += data.length
peer.last_read_time = Networking.milliseconds
true
rescue IO::WaitReadable
false
end
def write(peer:, packet:)
raw = packet.encode
@socket.send(raw, 0, peer.hostname, peer.port)
@total_packets_sent += 1
@total_data_sent += raw.length
@last_write_time = Networking.milliseconds
peer.total_packets_sent += 1
peer.total_data_sent += raw.length
peer.last_write_time = Networking.milliseconds
end
end
end
end

View File

@@ -1,94 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class Connection
attr_reader :address, :port
attr_accessor :total_packets_sent, :total_packets_received, :total_data_sent, :total_data_received, :last_read_time, :last_write_time
def initialize(address:, port:)
@address = address
@port = port
@read_buffer = ReadBuffer.new
@packet_write_queue = []
@peer_id = 0
@last_read_time = Networking.milliseconds
@last_write_time = Networking.milliseconds
@total_packets_sent = 0
@total_packets_received = 0
@total_data_sent = 0
@total_data_received = 0
@socket = nil
end
def connect
@socket = UDPSocket.new
@socket.connect(@address, @port)
send_packet(
Packet.new(
peer_id: 0,
sequence: 0,
type: Protocol::CONNECT,
payload: "Hello World!"
)
)
end
def send_packet(packet)
Packet.splinter(packet).each do |pkt|
@packet_write_queue << pkt
end
end
def update
while read
end
write
# puts "#{Networking.milliseconds} Total sent: #{@total_packets_sent} packets, #{@total_data_sent} data"
# puts "#{Networking.milliseconds} Total received: #{@total_packets_received} packets, #{@total_data_received} data"
@read_buffer.reconstruct_packets.each do |packet, _addr_info|
@peer_id = packet.payload.unpack1("C") if packet.peer_id.zero? && packet.type == Protocol::VERIFY_CONNECT
end
if @peer_id.positive? && Networking.milliseconds - @last_read_time >= Protocol::HEARTBEAT_INTERVAL
send_packet(Packet.new(peer_id: @peer_id, sequence: 0, type: Protocol::HEARTBEAT, payload: ""))
end
end
def close
@socket&.close
end
private
def read
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
@read_buffer.add(data, addr)
@total_packets_received += 1
@total_data_received += data.length
@last_read_time = Networking.milliseconds
true
rescue IO::WaitReadable
false
end
def write
while (packet = @packet_write_queue.shift)
@socket.send(packet.encode, 0, @address, @port)
@total_data_sent += packet.encode.length
@total_packets_sent += 1
end
end
end
end
end

View File

@@ -1,63 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class Director
attr_reader :tick_rate, :storage, :map, :server, :connection
def initialize(tick_rate: 15)
@tick_rate = (1000.0 / tick_rate) / 1000.0
@last_tick_time = CyberarmEngine::Networking.milliseconds
@directing = true
@storage = {}
@map = nil
end
def host_server(hostname: CyberarmEngine::Networking::DEFAULT_SERVER_HOSTNAME, port: CyberarmEngine::Networking::DEFAULT_SERVER_PORT, max_peers: CyberarmEngine::Networking::DEFAULT_PEER_LIMIT)
@server = Server.new(hostname: hostname, port: port, max_peers: max_peers)
@server.bind
end
def connect(hostname:, port: CyberarmEngine::Networking::DEFAULT_SERVER_PORT)
@connection = Connection.new(hostname: hostname, port: port)
@connection.connect
end
def load_map(map_parser:)
# TODO: send map_change to clients
@map = Map.new(map_parser: map_parser)
@map.setup
end
def run
Thread.start do
while @directing
dt = milliseconds - @last_tick_time
tick(dt)
@last_tick_time = milliseconds
sleep(@tick_rate)
end
end
end
def tick(delta_time)
return unless @map
Publisher.instance.publish(:tick, delta_time * 1000.0)
@map.update
@server&.update
@connection&.update
end
def shutdown
@directing = false
@server&.close
@connection&.close
end
end
end
end

View File

@@ -1,13 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module Networking
module Events
def on_connect(client)
end
def on_disconnect(client)
end
end
end
end

View File

@@ -1,8 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class PacketHandler
end
end
end

View File

@@ -1,8 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class SnapshotPacket < CyberarmEngine::Networking::Packet
end
end
end

View File

@@ -1,35 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class ReadBuffer
def initialize
@buffer = []
end
def add(buffer, addr_info)
@buffer << { buffer: buffer, addr_info: addr_info }
end
def reconstruct_packets
pairs = []
@buffer.each do |hash|
buffer = hash[:buffer]
addr = hash[:addr_info]
packet = Packet.from_stream(buffer)
if true # packet.valid?
pairs << [packet, addr]
@buffer.delete(hash)
else
puts "Invalid packet: #{packet}"
@buffer.delete(buffer)
end
end
pairs
end
end
end
end

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class Server < CyberarmEngine::Networking::Server
def connected(peer:)
end
def disconnected(peer:, reason:)
end
def packet_received(peer:, message:, channel:)
end
end
end
end

View File

@@ -1,64 +0,0 @@
# frozen_string_literal: true
class IMICFPS
class Overlay
include CommonMethods
Slot = Struct.new(:value, :width)
def initialize
@text = CyberarmEngine::Text.new("", x: 3, y: 3, border_color: Gosu::Color::BLACK)
@slots = []
@space_width = @text.textobject.text_width(" ")
end
def draw
return if @text.text.empty?
width = @text.markup_width + 8
Gosu.draw_rect(0, 0, width, (@text.height + 4), Gosu::Color.rgba(0, 0, 0, 100))
Gosu.draw_rect(2, 2, width - 4, (@text.height + 4) - 4, Gosu::Color.rgba(100, 100, 100, 100))
@text.draw
end
def update
rebuild_slots
end
def rebuild_slots
@slots.clear
if window.config.get(:options, :fps)
create_slot "FPS: #{Gosu.fps}"
if window.config.get(:debug_options, :stats)
create_slot "Frame time: #{(Gosu.milliseconds - window.delta_time).to_s.rjust(3, '0')}ms"
end
end
if window.config.get(:debug_options, :stats)
create_slot "Vertices: #{formatted_number(window.renderer.opengl_renderer.number_of_vertices)}"
create_slot "Faces: #{formatted_number(window.renderer.opengl_renderer.number_of_vertices / 3)}"
end
if window.config.get(:debug_options, :boundingboxes)
create_slot "Boundingboxes: #{window.config.get(:debug_options, :boundingboxes) ? 'On' : 'Off'}"
end
if window.config.get(:debug_options, :wireframe)
create_slot "Wireframes: #{window.config.get(:debug_options, :wireframe) ? 'On' : 'Off'}"
end
@text.text = ""
@slots.each_with_index do |slot, i|
@text.text += "#{slot.value} <c=ff000000>•</c> " unless i == @slots.size - 1
@text.text += slot.value.to_s if i == @slots.size - 1
end
end
def create_slot(string)
@slots << Slot.new(string, @text.textobject.text_width(string))
end
end
end

View File

@@ -1,10 +1,7 @@
# frozen_string_literal: true
class IMICFPS class IMICFPS
class Publisher class Publisher
def self.subscribe(subscription) def self.subscribe(subscription)
raise "Expected IMICFPS::Subscription not #{subscription.class}" unless subscription.is_a?(IMICFPS::Subscription) raise "Expected IMICFPS::Subscription not #{subscription.class}" unless subscription.is_a?(IMICFPS::Subscription)
Publisher.instance.add_sub(subscription) Publisher.instance.add_sub(subscription)
end end
@@ -24,14 +21,13 @@ class IMICFPS
def add_sub(subscription) def add_sub(subscription)
raise "Expected IMICFPS::Subscription not #{subscription.class}" unless subscription.is_a?(IMICFPS::Subscription) raise "Expected IMICFPS::Subscription not #{subscription.class}" unless subscription.is_a?(IMICFPS::Subscription)
@events[subscription.event] ||= [] @events[subscription.event] ||= []
@events[subscription.event] << subscription @events[subscription.event] << subscription
end end
def publish(event, context, *args) def publish(event, context, *args)
if subscribers = @events[event] if subscribers = @events.dig(event)
return unless event_handler = EventHandler.get(event) return unless event_handler = EventHandler.get(event)
subscribers.each do |subscriber| subscribers.each do |subscriber|

View File

@@ -0,0 +1,238 @@
class IMICFPS
class BoundingBoxRenderer
attr_reader :bounding_boxes, :vertex_count
def initialize(map:)
@map = map
@bounding_boxes = {}
@vertex_count = 0
end
def create_bounding_box(object, box, color = nil, mesh_object_id)
color ||= object.debug_color
if @bounding_boxes[mesh_object_id]
if @bounding_boxes[mesh_object_id][:color] != color
@bounding_boxes[mesh_object_id][:colors] = mesh_colors(color).pack("f*")
@bounding_boxes[mesh_object_id][:color] = color
return
else
return
end
end
@bounding_boxes[mesh_object_id] = {object: object, box: box, color: color, objects: []}
box = object.normalize_bounding_box
normals = mesh_normals
colors = mesh_colors(color)
vertices = mesh_vertices(box)
@vertex_count+=vertices.size
@bounding_boxes[mesh_object_id][:vertices_size] = vertices.size
@bounding_boxes[mesh_object_id][:vertices] = vertices.pack("f*")
@bounding_boxes[mesh_object_id][:normals] = normals.pack("f*")
@bounding_boxes[mesh_object_id][:colors] = colors.pack("f*")
object.model.objects.each do |mesh|
data = {}
box = mesh.bounding_box.normalize(object)
normals = mesh_normals
colors = mesh_colors(mesh.debug_color)
vertices = mesh_vertices(box)
@vertex_count+=vertices.size
data[:vertices_size] = vertices.size
data[:vertices] = vertices.pack("f*")
data[:normals] = normals.pack("f*")
data[:colors] = colors.pack("f*")
@bounding_boxes[mesh_object_id][:objects] << data
end
end
def mesh_normals
[
0,1,0,
0,1,0,
0,1,0,
0,1,0,
0,1,0,
0,1,0,
0,-1,0,
0,-1,0,
0,-1,0,
0,-1,0,
0,-1,0,
0,-1,0,
0,0,1,
0,0,1,
0,0,1,
0,0,1,
0,0,1,
0,0,1,
1,0,0,
1,0,0,
1,0,0,
1,0,0,
1,0,0,
1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0,
-1,0,0
]
end
def mesh_colors(color)
[
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue,
color.red, color.green, color.blue
]
end
def mesh_vertices(box)
[
box.min.x, box.max.y, box.max.z,
box.min.x, box.max.y, box.min.z,
box.max.x, box.max.y, box.min.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.min.z,
box.max.x, box.min.y, box.min.z,
box.max.x, box.min.y, box.max.z,
box.min.x, box.min.y, box.max.z,
box.max.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.max.z,
box.min.x, box.max.y, box.max.z,
box.min.x, box.max.y, box.min.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.max.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.min.z,
box.max.x, box.min.y, box.min.z,
box.max.x, box.min.y, box.max.z,
box.max.x, box.min.y, box.min.z,
box.max.x, box.max.y, box.max.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.max.y, box.max.z,
box.max.x, box.min.y, box.max.z,
box.min.x, box.max.y, box.max.z,
box.max.x, box.min.y, box.max.z,
box.min.x, box.min.y, box.max.z,
box.max.x, box.min.y, box.min.z,
box.min.x, box.min.y, box.min.z,
box.min.x, box.max.y, box.min.z,
box.max.x, box.min.y, box.min.z,
box.min.x, box.max.y, box.min.z,
box.max.x, box.max.y, box.min.z
]
end
def draw_bounding_boxes
@bounding_boxes.each do |key, bounding_box|
glPushMatrix
glTranslatef(bounding_box[:object].position.x, bounding_box[:object].position.y, bounding_box[:object].position.z)
draw_bounding_box(bounding_box)
@bounding_boxes[key][:objects].each {|o| draw_bounding_box(o)}
glPopMatrix
found = @map.entities.detect { |o| o == bounding_box[:object] }
unless found
@vertex_count -= @bounding_boxes[key][:vertices_size]
@bounding_boxes[key][:objects].each {|o| @vertex_count -= o[:vertices_size]}
@bounding_boxes.delete(key)
end
end
end
def draw_bounding_box(bounding_box)
glEnableClientState(GL_VERTEX_ARRAY)
glEnableClientState(GL_COLOR_ARRAY)
glEnableClientState(GL_NORMAL_ARRAY)
glVertexPointer(3, GL_FLOAT, 0, bounding_box[:vertices])
glColorPointer(3, GL_FLOAT, 0, bounding_box[:colors])
glNormalPointer(GL_FLOAT, 0, bounding_box[:normals])
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glDisable(GL_LIGHTING)
glDrawArrays(GL_TRIANGLES, 0, bounding_box[:vertices_size]/3)
glEnable(GL_LIGHTING)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)
glDisableClientState(GL_NORMAL_ARRAY)
end
end
end

163
lib/renderer/g_buffer.rb Normal file
View File

@@ -0,0 +1,163 @@
class IMICFPS
class GBuffer
include CommonMethods
attr_reader :screen_vbo, :vertices, :uvs
def initialize
@framebuffer = nil
@buffers = [:position, :diffuse, :normal, :texcoord, :scene]
@textures = {}
@screen_vbo = nil
@ready = false
@vertices = [
-1.0, -1.0, 0,
1.0, -1.0, 0,
-1.0, 1.0, 0,
-1.0, 1.0, 0,
1.0, -1.0, 0,
1.0, 1.0, 0,
].freeze
@uvs = [
0, 0,
1, 0,
0, 1,
0, 1,
1, 0,
1, 1
].freeze
create_framebuffer
create_screen_vbo
end
def width
window.width
end
def height
window.height
end
def create_framebuffer
buffer = ' ' * 4
glGenFramebuffers(1, buffer)
@framebuffer = buffer.unpack('L2').first
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer)
create_textures
status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
if status != GL_FRAMEBUFFER_COMPLETE
message = ""
case status
when GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT
message = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"
when GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT
message = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"
when GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER
message = "GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER"
when GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER
message = "GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER"
when GL_FRAMEBUFFER_UNSUPPORTED
message = "GL_FRAMEBUFFER_UNSUPPORTED"
else
message = "Unknown error!"
end
puts "Incomplete framebuffer: #{status}\nError: #{message}"
else
@ready = true
end
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
end
def create_textures
@buffers.size.times do |i|
buffer = ' ' * 4
glGenTextures(1, buffer)
texture_id = buffer.unpack('L2').first
@textures[@buffers[i]] = texture_id
glBindTexture(GL_TEXTURE_2D, texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, width, height, 0, GL_RGBA, GL_FLOAT, nil)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, texture_id, 0)
end
buffer = ' ' * 4
glGenTextures(1, buffer)
texture_id = buffer.unpack('L2').first
@textures[:depth] = texture_id
glBindTexture(GL_TEXTURE_2D, texture_id)
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nil)
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texture_id, 0)
draw_buffers = @buffers.each_with_index.map { |b ,i| Object.const_get("GL_COLOR_ATTACHMENT#{i}") }
glDrawBuffers(draw_buffers.size, draw_buffers.pack("I*"))
end
def create_screen_vbo
buffer = ' ' * 4
glGenVertexArrays(1, buffer)
@screen_vbo = buffer.unpack('L2').first
buffer = " " * 4
glGenBuffers(1, buffer)
@positions_buffer_id = buffer.unpack('L2').first
buffer = " " * 4
glGenBuffers(1, buffer)
@uvs_buffer_id = buffer.unpack('L2').first
glBindVertexArray(@screen_vbo)
glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id)
glBufferData(GL_ARRAY_BUFFER, @vertices.size * Fiddle::SIZEOF_FLOAT, @vertices.pack("f*"), GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nil)
glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id)
glBufferData(GL_ARRAY_BUFFER, @uvs.size * Fiddle::SIZEOF_FLOAT, @uvs.pack("f*"), GL_STATIC_DRAW);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, nil)
glBindVertexArray(0)
end
def bind_for_writing
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer)
end
def bind_for_reading
glBindFramebuffer(GL_READ_FRAMEBUFFER, @framebuffer)
end
def set_read_buffer(buffer)
glReadBuffer(GL_COLOR_ATTACHMENT0 + @textures.keys.index(buffer))
end
def unbind_framebuffer
glBindFramebuffer(GL_FRAMEBUFFER, 0)
end
def texture(type)
@textures[type]
end
def clean_up
glDeleteFramebuffers(1, [@framebuffer].pack("L"))
glDeleteTextures(@textures.values.size, @textures.values.pack("L*"))
glDeleteBuffers(2, [@positions_buffer_id, @uvs_buffer_id].pack("L*"))
glDeleteVertexArrays(1, [@screen_vbo].pack("L"))
gl_error?
end
end
end

View File

@@ -0,0 +1,239 @@
class IMICFPS
class OpenGLRenderer
include CommonMethods
@@immediate_mode_warning = false
def initialize
@g_buffer = GBuffer.new
end
def canvas_size_changed
@g_buffer.unbind_framebuffer
@g_buffer.clean_up
@g_buffer = GBuffer.new
end
def render(camera, lights, entities)
if Shader.available?("default") && Shader.available?("render_screen") && Shader.available?("lighting")
@g_buffer.bind_for_writing
gl_error?
glClearColor(0.0, 0.0, 0.0, 0.0)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
Shader.use("default") do |shader|
entities.each do |entity|
next unless entity.visible && entity.renderable
shader.uniform_transform("projection", camera.projection_matrix)
shader.uniform_transform("view", camera.view_matrix)
shader.uniform_transform("model", entity.model_matrix)
shader.uniform_boolean("hasTexture", entity.model.has_texture?)
shader.uniform_vec3("cameraPosition", camera.position)
gl_error?
draw_model(entity.model, shader)
entity.draw
end
end
lighting(lights)
post_processing
@g_buffer.unbind_framebuffer
gl_error?
@g_buffer.bind_for_reading
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
render_framebuffer
@g_buffer.unbind_framebuffer
gl_error?
else
puts "Shader 'default' failed to compile, using immediate mode for rendering..." unless @@immediate_mode_warning
@@immediate_mode_warning = true
gl_error?
lights.each(&:draw)
camera.draw
glEnable(GL_NORMALIZE)
entities.each do |entity|
next unless entity.visible && entity.renderable
glPushMatrix
glTranslatef(entity.position.x, entity.position.y, entity.position.z)
glScalef(entity.scale.x, entity.scale.y, entity.scale.z)
glRotatef(entity.orientation.x, 1.0, 0, 0)
glRotatef(entity.orientation.y, 0, 1.0, 0)
glRotatef(entity.orientation.z, 0, 0, 1.0)
gl_error?
draw_mesh(entity.model)
entity.draw
glPopMatrix
end
end
gl_error?
end
def lighting(lights)
if Shader.available?("lighting")
Shader.use("lighting") do |shader|
lights.each do |light|
shader.uniform_integer("inLightType", light.type);
shader.uniform_vec3("inLightPosition", light.position)
shader.uniform_vec3("inLightAmbient", light.ambient)
shader.uniform_vec3("inLightDiffuse", light.diffuse)
shader.uniform_vec3("inLightSpecular", light.specular)
glBindVertexArray(@g_buffer.screen_vbo)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glDisable(GL_DEPTH_TEST)
glEnable(GL_BLEND)
glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
glDisableVertexAttribArray(1)
glDisableVertexAttribArray(0)
glBindVertexArray(0)
end
gl_error?
end
end
end
def post_processing
end
def render_framebuffer
if Shader.available?("render_screen")
Shader.use("render_screen") do |shader|
glBindVertexArray(@g_buffer.screen_vbo)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glDisable(GL_DEPTH_TEST)
glEnable(GL_BLEND)
glActiveTexture(GL_TEXTURE0)
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:scene))
glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
glDisableVertexAttribArray(1)
glDisableVertexAttribArray(0)
glBindVertexArray(0)
end
end
end
def draw_model(model, shader)
glBindVertexArray(model.vertex_array_id)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glEnableVertexAttribArray(2)
if model.has_texture?
glBindTexture(GL_TEXTURE_2D, model.materials[model.textured_material].texture_id)
glEnableVertexAttribArray(3)
glEnableVertexAttribArray(4)
end
if window.config.get(:debug_options, :wireframe)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
Shader.active_shader.uniform_boolean("disableLighting", true)
glDrawArrays(GL_TRIANGLES, 0, model.faces.count * 3)
window.number_of_vertices += model.faces.count * 3
Shader.active_shader.uniform_boolean("disableLighting", false)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
end
glDrawArrays(GL_TRIANGLES, 0, model.faces.count * 3)
window.number_of_vertices += model.faces.count * 3
if model.has_texture?
glDisableVertexAttribArray(4)
glDisableVertexAttribArray(3)
glBindTexture(GL_TEXTURE_2D, 0)
end
glDisableVertexAttribArray(2)
glDisableVertexAttribArray(1)
glDisableVertexAttribArray(0)
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)
end
def draw_mesh(model)
model.objects.each_with_index do |o, i|
glEnable(GL_CULL_FACE) if model.entity.backface_culling
glEnable(GL_COLOR_MATERIAL)
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
glShadeModel(GL_FLAT) unless o.faces.first[4]
glShadeModel(GL_SMOOTH) if o.faces.first[4]
glEnableClientState(GL_VERTEX_ARRAY)
glEnableClientState(GL_COLOR_ARRAY)
glEnableClientState(GL_NORMAL_ARRAY)
if model.has_texture?
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, model.materials[model.textured_material].texture_id)
glEnableClientState(GL_TEXTURE_COORD_ARRAY)
glTexCoordPointer(3, GL_FLOAT, 0, o.flattened_textures)
end
glVertexPointer(4, GL_FLOAT, 0, o.flattened_vertices)
glColorPointer(3, GL_FLOAT, 0, o.flattened_materials)
glNormalPointer(GL_FLOAT, 0, o.flattened_normals)
if window.config.get(:debug_options, :wireframe) # This is kinda expensive
glDisable(GL_LIGHTING)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glPolygonOffset(2, 0.5)
glLineWidth(3)
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4)
window.number_of_vertices+=model.vertices.size
glLineWidth(1)
glPolygonOffset(0, 0)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
glEnable(GL_LIGHTING)
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4)
window.number_of_vertices+=model.vertices.size
else
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4)
window.number_of_vertices+=model.vertices.size
end
# glBindBuffer(GL_ARRAY_BUFFER, 0)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)
glDisableClientState(GL_NORMAL_ARRAY)
if model.has_texture?
glDisableClientState(GL_TEXTURE_COORD_ARRAY)
glDisable(GL_TEXTURE_2D)
end
glDisable(GL_CULL_FACE) if model.entity.backface_culling
glDisable(GL_COLOR_MATERIAL)
end
end
end
end

38
lib/renderer/renderer.rb Normal file
View File

@@ -0,0 +1,38 @@
class IMICFPS
class Renderer
include CommonMethods
attr_reader :opengl_renderer, :bounding_box_renderer
def initialize
# @bounding_box_renderer = BoundingBoxRenderer.new(map: map)
@opengl_renderer = OpenGLRenderer.new
end
def preload_default_shaders
shaders = ["default", "render_screen", "lighting"]
shaders.each do |shader|
Shader.new(
name: shader,
includes_dir: "shaders/include",
vertex: "shaders/vertex/#{shader}.glsl",
fragment: "shaders/fragment/#{shader}.glsl"
)
end
end
def draw(camera, lights, entities)
glViewport(0, 0, window.width, window.height)
glEnable(GL_DEPTH_TEST)
@opengl_renderer.render(camera, lights, entities)
end
def canvas_size_changed
@opengl_renderer.canvas_size_changed
end
def finalize # cleanup
end
end
end

Some files were not shown because too many files have changed in this diff Show More