1 Commits

Author SHA1 Message Date
c18309614f Stubbed out files for networking 2020-03-26 14:25:55 -05:00
198 changed files with 3456 additions and 16679 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

12
Gemfile
View File

@@ -1,15 +1,9 @@
# frozen_string_literal: true
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 "rake"
gem "cyberarm_engine", git: "https://github.com/cyberarm/cyberarm_engine"
gem "nokogiri", ">= 1.11.0.rc1"
gem "async-websocket"
group(:packaging) do
gem "excon"
gem "ocra"
gem "releasy", github: "gosu/releasy"
gem "rubyzip"
end

View File

@@ -1,57 +1,32 @@
GIT
remote: https://github.com/cyberarm/cyberarm_engine
revision: d1d87db070578fefe97f275b63157b4212a44a89
revision: d8551c7428da98bb7da76c138e5fbde50ef0137f
specs:
cyberarm_engine (0.22.0)
excon (~> 0.88)
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)
cyberarm_engine (0.13.1)
gosu (~> 0.15.0)
GEM
remote: https://rubygems.org/
specs:
concurrent-ruby (1.1.10)
cri (2.1.0)
excon (0.96.0)
gosu (1.4.5)
gosu_more_drawables (0.3.1)
i18n (1.12.0)
concurrent-ruby (~> 1.0)
mini_portile2 (2.8.1)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
gosu (0.15.1)
gosu (0.15.1-x64-mingw32)
mini_portile2 (2.4.0)
nokogiri (1.11.0.rc1)
mini_portile2 (~> 2.4.0)
nokogiri (1.11.0.rc1-x64-mingw32)
mini_portile2 (~> 2.4.0)
ocra (1.3.11)
opengl-bindings (1.6.13)
racc (1.6.2)
rake (13.0.6)
rubyzip (2.3.2)
opengl-bindings (1.6.9)
PLATFORMS
ruby
x64-mingw-ucrt
x64-mingw32
DEPENDENCIES
cyberarm_engine!
excon
i18n
nokogiri (>= 1.11.0.rc1)
ocra
opengl-bindings
rake
releasy!
rubyzip
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
![logo](https://raw.githubusercontent.com/cyberarm/i-mic-fps/master/svg/logo.svg)
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)
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/))
## 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
* `bundle install`
* `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
* `--native` - Launch in fullscreen using primary displays resolution
* `--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 "bundler/setup" # Releasy requires that your application uses bundler.
require 'bundler/setup' # Releasy requires that your application uses bundler.
require_relative "lib/version"
Releasy::Project.new do
name IMICFPS::NAME
version IMICFPS::VERSION
name "I-MIC FPS"
version "#{IMICFPS::VERSION}"
executable "i-mic-fps.rb"
files ["lib/**/*.*", "assets/**/*.*", "blends/**/*.*", "shaders/**/*.*", "static/**/*.*", "maps/**/*.*", "data/**/*.*"]
@@ -20,7 +12,7 @@ Releasy::Project.new do
verbose
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.
add_package :exe # Windows self-extracting archive.
end

View File

@@ -1,21 +1,16 @@
# frozen_string_literal: true
begin
require_relative "../cyberarm_engine/lib/cyberarm_engine"
rescue LoadError
require "cyberarm_engine"
end
require "gosu"
require_relative "lib/objects/text"
class Window < Gosu::Window
def initialize
super(Gosu.screen_width, Gosu.screen_height, fullscreen: true)
CyberarmEngine::Window.instance = self
$window = self
@size = 50
@slope = 250
@color_step = 10
@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)
@singleplayer = CyberarmEngine::Text.new("Singleplayer", color: Gosu::Color.rgb(0, 127, 127), size: 50, x: 0, y: 150, alignment: :center)
@title = Text.new("I-MIC FPS", color: Gosu::Color.rgb(255,127,0), size: 100, x: 0, y: 15, alignment: :center)
@singleplayer = Text.new("Singleplayer", color: Gosu::Color.rgb(0,127,127), size: 50, x: 0, y: 150, alignment: :center)
end
def draw

View File

@@ -1,11 +1,9 @@
# frozen_string_literal: true
origin = entity.position
on.entity_moved do |event|
entity.position = if origin.distance3d(event.entity.position) <= 3.0
origin + Vector.up * 2.4
if origin.distance3d(event.entity.position) <= 3.0
entity.position = origin + Vector.up * 2.4
else
origin
entity.position = origin
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,5 +1,3 @@
# frozen_string_literal: true
component(:building)
on.create do |event|

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
component(:vehicle) # Generic, Weapon
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)
# event.player.enter_vehicle
# 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,5 +1,3 @@
# frozen_string_literal: true
component(:building)
on.create do |event|
@@ -8,6 +6,6 @@ on.create do |event|
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_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(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"]))
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,14 @@
# frozen_string_literal: true
require "async/websocket"
IMICFPS_SERVER_MODE = true
require_relative "i-mic-fps"
require_relative "lib/networking/director"
require_relative "lib/networking/packet_handler"
require_relative "lib/networking/server"
require_relative "lib/networking/client"
director = IMICFPS::Networking::Director.new(mode: :server, hostname: "0.0.0.0", port: 56_789, interface: IMICFPS::Networking::MemoryServer)
require_relative "lib/networking/backends/memory_server"
require_relative "lib/networking/backends/memory_connection"
director = IMICFPS::Networking::Director.new(mode: :server, hostname: "0.0.0.0", port: 56789, interface: IMICFPS::Networking::MemoryServer)
director.define_singleton_method(:tick) do |dt|
puts "Ticked: #{dt}"
end

View File

@@ -1,114 +1,136 @@
# frozen_string_literal: true
require "fiddle"
require "yaml"
require "json"
require "abbrev"
require "time"
require "socket"
require "tmpdir"
require "securerandom"
require "opengl"
require "glu"
require "nokogiri"
require "i18n"
begin
require_relative "../cyberarm_engine/lib/cyberarm_engine"
require_relative "../cyberarm_engine/lib/cyberarm_engine/opengl"
rescue LoadError => e
pp e
require "cyberarm_engine"
require "cyberarm_engine/opengl"
end
Dir.chdir(File.dirname(__FILE__))
require_relative "lib/ext/numeric"
require_relative "lib/ext/load_opengl"
include CyberarmEngine
include OpenGL
include GLU
def require_all(directory)
files = Dir["#{directory}/**/*.rb"].sort!
file_order = []
require_relative "lib/version"
require_relative "lib/constants"
require_relative "lib/common_methods"
loop do
failed = []
first_name_error = nil
require_relative "lib/trees/aabb_tree_debug"
require_relative "lib/trees/aabb_tree"
require_relative "lib/trees/aabb_node"
files.each do |file|
begin
require_relative file
file_order << file
rescue NameError => e
failed << file
first_name_error ||= e
end
require_relative "lib/managers/input_mapper"
require_relative "lib/managers/entity_manager"
require_relative "lib/managers/light_manager"
require_relative "lib/managers/network_manager"
require_relative "lib/managers/collision_manager"
require_relative "lib/managers/physics_manager"
require_relative "lib/renderer/renderer"
require_relative "lib/renderer/g_buffer"
require_relative "lib/renderer/opengl_renderer"
require_relative "lib/renderer/bounding_box_renderer"
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
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"
if failed.size == files.size
raise first_name_error
else
files = failed
end
break if failed.empty?
end
require_relative "lib/states/game_states/game"
require_relative "lib/states/game_states/loading_state"
# pp file_order.map { |f| f.gsub(".rb", "")}
end
require_relative "lib/subscription"
require_relative "lib/publisher"
require_relative "lib/event"
require_relative "lib/event_handler"
require_relative "lib/event_handlers/input"
require_relative "lib/event_handlers/entity_moved"
require_relative "lib/event_handlers/entity_lifecycle"
require_all "lib"
require_relative "lib/scripting"
require_relative "lib/scripting/sandbox"
require_relative "lib/scripting/whitelist"
# Don't launch game if IMICFPS_SERVER_MODE is defined
# or if game is being packaged
def prevent_launch?
packaging_lockfile = File.expand_path("i-mic-fps-packaging.lock", Dir.tmpdir)
m = "Game client not launched"
require_relative "lib/component"
require_relative "lib/components/building"
return [true, "#{m}: Server is running"] if defined?(IMICFPS_SERVER_MODE) && IMICFPS_SERVER_MODE
require_relative "lib/game_objects/entity"
require_relative "lib/game_objects/light"
require_relative "lib/game_objects/particle_emitter"
return [true, "#{m}: Packaging is running"] if defined?(Ocra)
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"
if File.exist?(packaging_lockfile) && File.read(packaging_lockfile).strip == IMICFPS::VERSION
return [true, "#{m}: Packaging lockfile is present (#{packaging_lockfile})"]
end
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"
[false, ""]
end
require_relative "lib/model/parsers/wavefront_parser"
require_relative "lib/model/parsers/collada_parser"
if prevent_launch?[0]
puts prevent_launch?[1]
else
native = ARGV.join.include?("--native")
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
require_relative "lib/map_parser"
require_relative "lib/manifest"
require_relative "lib/map"
window = IMICFPS::Window.new(
width: window_width,
height: window_height,
fullscreen: window_fullscreen,
resizable: !window_fullscreen,
update_interval: 1000.0 / fps_target
)
require_relative "lib/scene"
require_relative "lib/scenes/turn_table"
require_relative "lib/crosshair"
require_relative "lib/demo"
require_relative "lib/networking/director"
require_relative "lib/networking/packet_handler"
require_relative "lib/networking/client"
require_relative "lib/networking/server"
require_relative "lib/networking/connection"
require_relative "lib/networking/backends/memory_server"
require_relative "lib/networking/backends/memory_connection"
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
window.show
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!"
raise
end
else
window.show
end
IMICFPS::Window.new.show
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
def self.assets_path
File.expand_path("../assets", __dir__)
File.expand_path("./../../assets", __FILE__)
end
module CommonMethods
def window
CyberarmEngine::Window.instance
end
def delta_time
(Gosu.milliseconds - window.delta_time) / 1000.0
end
def window; $window; end
def button_down?(id)
window.button_down?(id)
end
def delta_time; (Gosu.milliseconds - window.delta_time) / 1000.0; end
def button_down?(id); window.button_down?(id); end
def mouse_x
window.mouse_x
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 mouse_x; window.mouse_x; 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)
window.gl do
@@ -43,79 +24,32 @@ class IMICFPS
def formatted_number(number)
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
def control_down?
button_down?(Gosu::KbLeftControl) || button_down?(Gosu::KbRightControl)
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 control_down?; button_down?(Gosu::KbLeftControl) || button_down?(Gosu::KbRightControl); 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)
window.draw_rect(*args)
end
def draw_quad(*args)
window.draw_quad(*args)
end
def fill(color = Gosu::Color::WHITE, z = 0)
draw_rect(0, 0, window.width, window.height, color, z)
def fill(color = Gosu::Color::WHITE)
draw_rect(0, 0, window.width, window.height, color)
end
def fill_quad(x1, y1, x2, y2, x3, y3, x4, y4, color = Gosu::Color::WHITE, z = 0, mode = :default)
draw_quad(
x1, y1, color,
x2, y2, color,
x3, y3, color,
x4, y4, color,
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
Gosu.draw_quad(
0, 0, primary_color,
window.width, 0, primary_color,
window.width, window.height, accent_color,
0, window.height, accent_color,
-2
)
def gl_error?
e = glGetError()
if e != GL_NO_ERROR
$stderr.puts "OpenGL error detected by handler at: #{caller[0]}"
$stderr.puts " #{gluErrorString(e)} (#{e})\n"
exit if window.config.get(:debug_options, :opengl_error_panic)
end
end
end
end

View File

@@ -1,27 +1,25 @@
# frozen_string_literal: true
class IMICFPS
class Component
@components = {}
COMPONENTS = {}
def self.get(name)
@components[name]
COMPONENTS.dig(name)
end
def self.inherited(subclass)
@components["__pending"] ||= []
@components["__pending"] << subclass
COMPONENTS["__pending"] ||= []
COMPONENTS["__pending"] << subclass
end
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
@components[component.name] = component
COMPONENTS[component.name] = component
end
@components.delete("__pending")
COMPONENTS.delete("__pending")
end
def initialize
@@ -32,7 +30,9 @@ class IMICFPS
string = self.class.name.split("::").last
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
def setup

View File

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

View File

@@ -1,11 +1,10 @@
# frozen_string_literal: true
class IMICFPS
GAME_ROOT_PATH = File.expand_path("..", File.dirname(__FILE__))
SANS_FONT = "#{GAME_ROOT_PATH}/static/fonts/Cantarell/Cantarell-Regular.otf"
BOLD_SANS_FONT = "#{GAME_ROOT_PATH}/static/fonts/Cantarell/Cantarell-Bold.otf"
MONOSPACE_FONT = "#{GAME_ROOT_PATH}/static/fonts/Oxygen_Mono/OxygenMono-Regular.ttf"
TextureCoordinate = Struct.new(:u, :v, :weight)
Point = Struct.new(:x, :y)
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
MODEL_METER_SCALE = 1.0

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS
class Crosshair
include CommonMethods
@@ -11,25 +9,8 @@ class IMICFPS
end
def draw
draw_rect(
window.width / 2 - @size,
(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
)
draw_rect(window.width/2-@size, (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

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS
class Demo
def initialize(camera:, player:, demo:, mode:)
@@ -32,7 +30,7 @@ class IMICFPS
@file.puts("tick #{@index}")
end
@file.puts("down #{InputMapper.actions(id)}")
@file.puts("down #{InputMapper.action(id)}")
@changed = true
end
end
@@ -44,7 +42,7 @@ class IMICFPS
@file.puts("tick #{@index}")
end
@file.puts("up #{InputMapper.actions(id)}")
@file.puts("up #{InputMapper.action(id)}")
@changed = true
end
end
@@ -56,37 +54,33 @@ class IMICFPS
@tick += 1
end
def playing?
@mode == :play
end
def recording?
!playing?
end
def playing?; @mode == :play; end
def recording?; !playing?; end
def play
if @data[@index]&.start_with?("tick")
if @tick == @data[@index].split(" ").last.to_i
@index+=1
until @data[@index]&.start_with?("tick")
until(@data[@index]&.start_with?("tick"))
break unless @data[@index]
data = @data[@index].split(" ")
case data.first
when "up"
if data.first == "up"
input = InputMapper.get(data.last.to_sym)
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)
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
@player.orientation.y = (data[2].to_f * -1) - 180
else
# hmm
end
@index += 1

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,8 @@
# frozen_string_literal: true
class IMICFPS
class EventHandler
class Input < EventHandler
def handles
%i[button_down button_up]
[:button_down, :button_up]
end
def handle(subscriber, context, *args)
@@ -15,10 +13,12 @@ class IMICFPS
if action.is_a?(Numeric) && action == key
subscriber.trigger(event)
elsif InputMapper.get(action) == key
else
if InputMapper.get(action) == key
subscriber.trigger(event)
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
when :OPENGL_PLATFORM_WINDOWS
OpenGL.load_lib("opengl32.dll", "C:/Windows/System32")
@@ -30,8 +28,8 @@ when :OPENGL_PLATFORM_LINUX
OpenGL.load_lib("libGL.so", gl_library_path)
GLU.load_lib("libGLU.so", gl_library_path)
else
raise "Couldn't find GL libraries"
raise RuntimeError, "Couldn't find GL libraries"
end
else
raise "Unsupported platform."
raise RuntimeError, "Unsupported platform."
end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
if RUBY_VERSION < "2.5.0"
puts "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
puts "|NOTICE| Ruby is #{RUBY_VERSION} not 2.5.0+..............................|Notice|"
@@ -13,7 +11,7 @@ if RUBY_VERSION < "2.5.0"
elsif self > max
max
else
self
return self
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 Player < Entity
attr_accessor :speed
attr_reader :name, :bound_model
attr_reader :name, :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.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
def update
@@ -61,9 +117,19 @@ class IMICFPS
end
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
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,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS
class Skydome < Entity
def setup

View File

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

View File

@@ -1,12 +1,13 @@
# frozen_string_literal: true
class IMICFPS
# A game object is any renderable thing
class Entity
include CommonMethods
attr_accessor :visible, :renderable, :backface_culling, :position, :orientation, :scale, :velocity, :debug_color
attr_reader :name, :bounding_box, :drag, :camera, :manifest, :model
attr_accessor :visible, :renderable, :backface_culling
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)
@manifest = manifest
@@ -15,7 +16,7 @@ class IMICFPS
@position = map_entity.position.clone
@orientation = map_entity.orientation.clone
@scale = map_entity.scale.clone
bind_model
@bound_model = bind_model
elsif spawnpoint
@position = spawnpoint.position.clone
@orientation = spawnpoint.orientation.clone
@@ -44,7 +45,8 @@ class IMICFPS
setup
if @model
if @bound_model
@bound_model.model.entity = self
@normalized_bounding_box = normalize_bounding_box_with_offset
normalize_bounding_box
@@ -52,7 +54,7 @@ class IMICFPS
@camera = nil
self
return self
end
def load_scripts
@@ -66,15 +68,22 @@ class IMICFPS
end
def bind_model
model = ModelCache.find_or_cache(manifest: @manifest)
raise "model isn't a model!" unless model.is_a?(Model)
model = ModelCache.new(manifest: @manifest)
@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
return model
end
def model
@bound_model.model if @bound_model
end
def unbind_model
@model = nil
@bound_model = nil
end
def attach_camera(camera)
@@ -92,7 +101,10 @@ class IMICFPS
def draw
end
def update
model.update
unless at_same_position?
Publisher.instance.publish(:entity_moved, nil, self)
@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)
end
def debug_color=(color)
@debug_color = color
end
def at_same_position?
@position == @last_position
end
def normalize_bounding_box_with_offset
@model.bounding_box.normalize_with_offset(self)
@bound_model.model.bounding_box.normalize_with_offset(self)
end
def normalize_bounding_box
@model.bounding_box.normalize(self)
@bound_model.model.bounding_box.normalize(self)
end
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
def initialize(position:, image:, interval: 1_500, time_to_live: 3_000, max_particles: 500)
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 CollisionManager
attr_reader :map, :collisions
def initialize(map:)
@map = map
@collisions = {}
@@ -50,13 +47,13 @@ class IMICFPS
next if entity.manifest.collision_resolution == :static # Only dynamic entities can be resolved
search = @aabb_tree.search(entity.bounding_box)
if search.size.positive?
if search.size > 0
search.reject! {|ent| ent == entity || !ent.collidable?}
broadphase[entity] = search
end
end
broadphase.each do |_entity, _collisions|
broadphase.each do |entity, _collisions|
_collisions.each do |ent|
# aabb vs aabb
# 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
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
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.min -= local
box.max -= local
box
return box
end
def on_ground?(entity) # TODO: Use some form of caching to speed this up
@@ -98,7 +95,7 @@ class IMICFPS
broadphase.detect do |ent|
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
return true
end
@@ -107,7 +104,7 @@ class IMICFPS
break if on_ground
end
on_ground
return on_ground
end
end
end

View File

@@ -1,22 +1,18 @@
# frozen_string_literal: true
class IMICFPS
module EntityManager # Get included into GameState context
def add_entity(entity)
if @collision_manager && entity.manifest.collision
@collision_manager.add(entity)
end # Add every entity to collision manager
@collision_manager.add(entity) if @collision_manager && entity.manifest.collision# Add every entity to collision manager
Publisher.instance.publish(:create, nil, entity)
@entities << entity
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))
add_entity(IMICFPS::Entity.new(map_entity: ent, manifest: Manifest.new(package: package, name: name)))
end
def find_entity(entity)
@entities.detect { |e| e == entity }
@entities.detect {|entity| entity == entity}
end
def find_entity_by(name:)
@@ -24,12 +20,13 @@ class IMICFPS
end
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
@publisher.publish(:destroy, nil, entity)
@entities.delete(ent)
end
end
def entities
@entities

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS
class PhysicsManager
def initialize(collision_manager:)
@@ -17,7 +15,7 @@ class IMICFPS
end
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
def simulate
@@ -34,7 +32,7 @@ class IMICFPS
entity.velocity.y = 0
else
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

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

View File

@@ -1,13 +1,11 @@
# frozen_string_literal: true
class IMICFPS
class Map
include EntityManager
include LightManager
include CommonMethods
attr_reader :collision_manager, :gravity
attr_reader :collision_manager
attr_reader :gravity
def initialize(map_parser:, gravity: IMICFPS::EARTH_GRAVITY)
@map_parser = map_parser
@gravity = gravity
@@ -16,97 +14,24 @@ class IMICFPS
@lights = []
@collision_manager = CollisionManager.new(map: self)
@renderer = window.renderer
Publisher.new
end
def setup
add_terrain if @map_parser.terrain.name
add_skybox if @map_parser.skydome.name
add_lights
add_entities
add_entity(Terrain.new(map_entity: @map_parser.terrain, manifest: Manifest.new(package: @map_parser.terrain.package, name: @map_parser.terrain.name)))
# TODO: Add player entity from director
add_entity(
Player.new(
spawnpoint: @map_parser.spawnpoints.sample,
manifest: Manifest.new(
package: "base",
name: "character"
)
)
)
end
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))
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|
add_light(
Light.new(
id: available_light,
type: l.type,
position: l.position,
diffuse: l.diffuse,
ambient: l.ambient,
specular: l.specular,
intensity: l.intensity
)
)
add_light(Light.new(id: available_light, position: l.position, diffuse: l.diffuse, ambient: l.ambient, specular: l.specular, intensity: l.intensity))
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|
add_entity(
Entity.new(
map_entity: ent,
manifest: Manifest.new(
package: ent.package,
name: ent.name
)
)
)
add_entity(Entity.new(map_entity: ent, manifest: Manifest.new(package: ent.package, name: ent.name)))
end
add_entity(Player.new(spawnpoint: @map_parser.spawnpoints.sample, manifest: Manifest.new(package: "base", name: "character")))
end
def data
@@ -123,7 +48,7 @@ class IMICFPS
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen and the depth buffer
gl_error?
window.renderer.draw(camera, @lights, @entities)
@renderer.draw(camera, @lights, @entities)
end
end

View File

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

@@ -0,0 +1,6 @@
class IMICFPS
module Networking
class MemoryConnection < Connection
end
end
end

View File

@@ -0,0 +1,6 @@
class IMICFPS
module Networking
class MemoryServer < Server
end
end
end

18
lib/networking/client.rb Normal file
View File

@@ -0,0 +1,18 @@
class IMICFPS
module Networking
class Client
def initialize(socket:)
@socket = socket
end
def read
end
def write
end
def close
end
end
end
end

View File

@@ -1,93 +1,13 @@
# 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
def initialize(hostname:, port:)
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

View File

@@ -1,62 +1,60 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class Director
attr_reader :tick_rate, :storage, :map, :server, :connection
def initialize(tick_rate: 15)
attr_reader :mode, :hostname, :port, :tick_rate, :storage
def initialize(mode:, hostname:, port:, interface:, state: nil, tick_rate: 2)
@mode = mode
@hostname = hostname
@port = port
@state = state
@tick_rate = (1000.0 / tick_rate) / 1000.0
@last_tick_time = CyberarmEngine::Networking.milliseconds
case @mode
when :server
@server = interface.new(hostname: @hostname, port: @port)
when :connection
@connection = interface.new(hostname: @hostname, port: @port)
when :memory
@server = interface[:server].new(hostname: @hostname, port: @port)
@connection = interface[:connection].new(hostname: @hostname, port: @port)
else
raise ArgumentError, "Expected mode to be :server, :connection, or :memory, not #{mode.inspect}"
end
@last_tick_time = 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
Thread.start do |thread|
while(@directing)
dt = milliseconds - @last_tick_time
tick(dt)
@server.update if @server
@connection.update if @connection
@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
def tick(dt)
end
def shutdown
@directing = false
@server&.close
@connection&.close
@clients.each(&:close)
@server.update if @server
@connection.update if @connection
end
def milliseconds
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end
end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS
module Networking
module Events

18
lib/networking/packet.rb Normal file
View File

@@ -0,0 +1,18 @@
class IMICFPS
module Networking
class Packet
def initialize(type:, payload:)
end
def self.encode(packet)
"#{packet.type}|#{packet.payload}"
end
def self.decode(string)
split = string.split("|")
Packet.new(split.first, split.last)
end
end
end
end

View File

@@ -1,5 +1,3 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class PacketHandler

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,15 +1,25 @@
# frozen_string_literal: true
class IMICFPS
module Networking
class Server < CyberarmEngine::Networking::Server
def connected(peer:)
MAX_CLIENTS = 32
class Server
attr_reader :hostname, :port, :max_clients, :clients
def initialize(hostname:, port:, max_clients: MAX_CLIENTS)
@hostname = hostname
@port = port
@max_clients = max_clients
@clients = []
@socket = nil
end
def disconnected(peer:, reason:)
def bind
end
def packet_received(peer:, message:, channel:)
def broadcast(packet)
end
def close
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 Publisher
def self.subscribe(subscription)
raise "Expected IMICFPS::Subscription not #{subscription.class}" unless subscription.is_a?(IMICFPS::Subscription)
Publisher.instance.add_sub(subscription)
end
@@ -24,14 +21,13 @@ class IMICFPS
def add_sub(subscription)
raise "Expected IMICFPS::Subscription not #{subscription.class}" unless subscription.is_a?(IMICFPS::Subscription)
@events[subscription.event] ||= []
@events[subscription.event] << subscription
end
def publish(event, context, *args)
if subscribers = @events[event]
if subscribers = @events.dig(event)
return unless event_handler = EventHandler.get(event)
subscribers.each do |subscriber|

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