mirror of
https://github.com/cyberarm/i-mic-fps.git
synced 2025-12-15 07:32:35 +00:00
Compare commits
79 Commits
v0.1.0
...
4c3c549f27
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c3c549f27 | ||
| 2da9edb6d0 | |||
| 8f2d7ff905 | |||
| 580c9d79ce | |||
| 07c8dfa45b | |||
|
|
b69bdc8e46 | ||
| 3481de2498 | |||
|
|
7bcd973a88 | ||
| 1d34cbb63d | |||
|
|
2df2df8488 | ||
| 87bff4ab82 | |||
| cf37b94d80 | |||
| 0fe1d85924 | |||
| ec2b32ff92 | |||
| 3986b1b0af | |||
| 58b2f8b890 | |||
| 5cb48233fb | |||
| da54bf5c53 | |||
| 3570a80d67 | |||
| 51363d2e3d | |||
| a16e14b0e5 | |||
|
|
4ebc4772a1 | ||
| 15ba3fb15b | |||
| c5e0f33f21 | |||
| d3fbf5dcf5 | |||
| 5b662f83cf | |||
| 55bfe6ed79 | |||
| bd414cf765 | |||
| c848a11c12 | |||
| 5759055838 | |||
| f04217ccc7 | |||
| 5ebcc56c33 | |||
| 8420ccd364 | |||
| 95bea199ed | |||
| aa30ff73d0 | |||
| 791351f2f5 | |||
| 9aa5dc7174 | |||
| c7590366a6 | |||
| b9c30ade80 | |||
| ecbbc77ca7 | |||
| 85ec285263 | |||
| 4c8e6c3d5f | |||
| 9558370ab9 | |||
| e09bd06d24 | |||
| 87b4b8ef92 | |||
| 2a36c58abe | |||
| e69dd3402d | |||
| dd7a7ac602 | |||
| 985f7c331f | |||
| b6c64b4b6b | |||
| d91398b529 | |||
| f7945bf47d | |||
| d72e8ccbd0 | |||
| 4ee97cca4b | |||
| ce90284001 | |||
| 1d7cd19b41 | |||
| f6e4a509fd | |||
| b4a0a7a8bc | |||
| 65cfc1a124 | |||
| 9264ef6e58 | |||
| 635f4e3720 | |||
| 5827f0b907 | |||
| bac0311263 | |||
| 94c3dea7b9 | |||
| fbdea30015 | |||
| dae950c72a | |||
| 7c81dd93e3 | |||
| fa2873bb8f | |||
| f72a8d4c35 | |||
| d03c3ffcd8 | |||
| 644c1916b2 | |||
| a0f8ce4bfb | |||
| cf1e72225c | |||
| e94f2582f9 | |||
| bae6a6a332 | |||
| e3a2c9abe0 | |||
| bc695df4a1 | |||
| bc480f9fae | |||
| d212691b71 |
37
.github/workflows/ruby.yml
vendored
Normal file
37
.github/workflows/ruby.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# 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
|
||||
8
.rubocop.yml
Normal file
8
.rubocop.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
Style/StringLiterals:
|
||||
EnforcedStyle: double_quotes
|
||||
|
||||
Metrics/MethodLength:
|
||||
Max: 40
|
||||
|
||||
Style/EmptyMethod:
|
||||
EnforcedStyle: expanded
|
||||
14
Gemfile
14
Gemfile
@@ -1,13 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
gem "rake"
|
||||
gem "opengl-bindings", require: "opengl"
|
||||
gem "cyberarm_engine", git: "https://github.com/cyberarm/cyberarm_engine"
|
||||
gem "i18n"
|
||||
gem "nokogiri", ">= 1.11.0.rc1"
|
||||
gem "async-websocket"
|
||||
gem "opengl-bindings", require: "opengl"
|
||||
gem "rake"
|
||||
|
||||
group(:packaging) do
|
||||
gem "releasy", github: "gosu/releasy"
|
||||
gem "ocra"
|
||||
gem "rubyzip"
|
||||
gem "excon"
|
||||
gem "ocra"
|
||||
gem "releasy", github: "gosu/releasy"
|
||||
gem "rubyzip"
|
||||
end
|
||||
|
||||
69
Gemfile.lock
69
Gemfile.lock
@@ -1,9 +1,10 @@
|
||||
GIT
|
||||
remote: https://github.com/cyberarm/cyberarm_engine
|
||||
revision: 0850336e55891f1f10dcb10e3b4b42e5f7379b33
|
||||
revision: 72037efc735089cf1ff4b56ec57eb793699b27c6
|
||||
specs:
|
||||
cyberarm_engine (0.14.0)
|
||||
gosu (~> 0.15.0)
|
||||
cyberarm_engine (0.23.0)
|
||||
excon (~> 0.88)
|
||||
gosu (~> 1.1)
|
||||
gosu_more_drawables (~> 0.3)
|
||||
|
||||
GIT
|
||||
@@ -19,60 +20,32 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
async (1.26.0)
|
||||
console (~> 1.0)
|
||||
nio4r (~> 2.3)
|
||||
timers (~> 4.1)
|
||||
async-http (0.52.2)
|
||||
async (~> 1.25)
|
||||
async-io (~> 1.28)
|
||||
async-pool (~> 0.2)
|
||||
protocol-http (~> 0.19.0)
|
||||
protocol-http1 (~> 0.13.0)
|
||||
protocol-http2 (~> 0.14.0)
|
||||
async-io (1.29.0)
|
||||
async (~> 1.14)
|
||||
async-pool (0.3.1)
|
||||
async (~> 1.25)
|
||||
async-websocket (0.14.0)
|
||||
async-http (~> 0.51)
|
||||
async-io (~> 1.23)
|
||||
protocol-websocket (~> 0.7.0)
|
||||
console (1.8.2)
|
||||
concurrent-ruby (1.2.2)
|
||||
cri (2.1.0)
|
||||
excon (0.73.0)
|
||||
gosu (0.15.1)
|
||||
gosu (0.15.1-x64-mingw32)
|
||||
gosu_more_drawables (0.3.0)
|
||||
mini_portile2 (2.5.0)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.11.0.rc2)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
nokogiri (1.11.0.rc2-x64-mingw32)
|
||||
excon (0.99.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)
|
||||
ocra (1.3.11)
|
||||
opengl-bindings (1.6.10)
|
||||
protocol-hpack (1.4.2)
|
||||
protocol-http (0.19.0)
|
||||
protocol-http1 (0.13.0)
|
||||
protocol-http (~> 0.19)
|
||||
protocol-http2 (0.14.0)
|
||||
protocol-hpack (~> 1.4)
|
||||
protocol-http (~> 0.18)
|
||||
protocol-websocket (0.7.4)
|
||||
protocol-http (~> 0.2)
|
||||
protocol-http1 (~> 0.2)
|
||||
rake (13.0.1)
|
||||
rubyzip (2.3.0)
|
||||
timers (4.3.0)
|
||||
opengl-bindings (1.6.13)
|
||||
racc (1.6.2)
|
||||
rake (13.0.6)
|
||||
rubyzip (2.3.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x64-mingw-ucrt
|
||||
x64-mingw32
|
||||
|
||||
DEPENDENCIES
|
||||
async-websocket
|
||||
cyberarm_engine!
|
||||
excon
|
||||
i18n
|
||||
nokogiri (>= 1.11.0.rc1)
|
||||
ocra
|
||||
opengl-bindings
|
||||
@@ -81,4 +54,4 @@ DEPENDENCIES
|
||||
rubyzip
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
2.4.8
|
||||
|
||||
35
README.md
35
README.md
@@ -1,17 +1,46 @@
|
||||

|
||||

|
||||

|
||||
|
||||
# I-MIC FPS
|
||||

|
||||
|
||||
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/))
|
||||
|
||||

|
||||

|
||||
|
||||
## Using
|
||||
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.)
|
||||
Ruby 3.0+ interpeter with support for the Gosu game library C extension.
|
||||
* 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
|
||||
* `--mesa-override` - (Linux) Force MESA to use OpenGL/GLSL version 3.30
|
||||
* `--savedemo` - Record camera movement and key events to playback later *(alpha-quality feature)*
|
||||
* `--playdemo` - Plays the previously recorded demo *(alpha-quality feature)*
|
||||
* `--playdemo` - Plays the previously recorded demo *(alpha-quality feature)*
|
||||
|
||||
4
Rakefile
4
Rakefile
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
require "tmpdir"
|
||||
require "fileutils"
|
||||
@@ -5,7 +7,7 @@ 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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
begin
|
||||
require_relative "../cyberarm_engine/lib/cyberarm_engine"
|
||||
rescue LoadError
|
||||
@@ -7,31 +9,31 @@ end
|
||||
class Window < Gosu::Window
|
||||
def initialize
|
||||
super(Gosu.screen_width, Gosu.screen_height, fullscreen: true)
|
||||
$window = self
|
||||
CyberarmEngine::Window.instance = 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 = 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)
|
||||
end
|
||||
|
||||
def draw
|
||||
@background ||= Gosu.record(Gosu.screen_width, Gosu.screen_height) do
|
||||
((Gosu.screen_height+@slope)/@size).times do |i|
|
||||
((Gosu.screen_height + @slope) / @size).times do |i|
|
||||
fill_quad(
|
||||
0, i*@size,
|
||||
0, @slope+(i*@size),
|
||||
Gosu.screen_width/2, (-@slope)+(i*@size),
|
||||
Gosu.screen_width/2, i*@size,
|
||||
Gosu::Color.rgba(@base_color.red-i*@color_step, @base_color.green-i*@color_step, @base_color.blue-i*@color_step, 200)
|
||||
0, i * @size,
|
||||
0, @slope + (i * @size),
|
||||
Gosu.screen_width / 2, (-@slope) + (i * @size),
|
||||
Gosu.screen_width / 2, i * @size,
|
||||
Gosu::Color.rgba(@base_color.red - i * @color_step, @base_color.green - i * @color_step, @base_color.blue - i * @color_step, 200)
|
||||
)
|
||||
fill_quad(
|
||||
Gosu.screen_width, i*@size,
|
||||
Gosu.screen_width, @slope+(i*@size),
|
||||
Gosu.screen_width/2, (-@slope)+(i*@size),
|
||||
Gosu.screen_width/2, i*@size,
|
||||
Gosu::Color.rgba(@base_color.red-i*@color_step, @base_color.green-i*@color_step, @base_color.blue-i*@color_step, 200)
|
||||
Gosu.screen_width, i * @size,
|
||||
Gosu.screen_width, @slope + (i * @size),
|
||||
Gosu.screen_width / 2, (-@slope) + (i * @size),
|
||||
Gosu.screen_width / 2, i * @size,
|
||||
Gosu::Color.rgba(@base_color.red - i * @color_step, @base_color.green - i * @color_step, @base_color.blue - i * @color_step, 200)
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -40,8 +42,8 @@ class Window < Gosu::Window
|
||||
|
||||
# Box
|
||||
draw_rect(
|
||||
Gosu.screen_width/4, 0,
|
||||
Gosu.screen_width/2, Gosu.screen_height,
|
||||
Gosu.screen_width / 4, 0,
|
||||
Gosu.screen_width / 2, Gosu.screen_height,
|
||||
Gosu::Color.rgba(100, 100, 100, 150)
|
||||
# Gosu::Color.rgba(@base_color.red+@color_step, @base_color.green+@color_step, @base_color.blue+@color_step, 200)
|
||||
)
|
||||
@@ -53,21 +55,21 @@ class Window < Gosu::Window
|
||||
# Cursor
|
||||
fill_quad(
|
||||
mouse_x, mouse_y,
|
||||
mouse_x+16, mouse_y+16,
|
||||
mouse_x, mouse_y+16,
|
||||
mouse_x, mouse_y+16,
|
||||
mouse_x + 16, mouse_y + 16,
|
||||
mouse_x, mouse_y + 16,
|
||||
mouse_x, mouse_y + 16,
|
||||
Gosu::Color::RED, Float::INFINITY
|
||||
)
|
||||
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,
|
||||
x1, y1, color,
|
||||
x2, y2, color,
|
||||
x3, y3, color,
|
||||
x4, y4, color,
|
||||
z, mode
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def button_up(id)
|
||||
@@ -79,4 +81,4 @@ class Window < Gosu::Window
|
||||
end
|
||||
end
|
||||
|
||||
Window.new.show
|
||||
Window.new.show
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
origin = entity.position
|
||||
|
||||
on.entity_moved do |event|
|
||||
if origin.distance3d(event.entity.position) <= 3.0
|
||||
entity.position = origin + Vector.up * 2.4
|
||||
else
|
||||
entity.position = origin
|
||||
end
|
||||
end
|
||||
entity.position = if origin.distance3d(event.entity.position) <= 3.0
|
||||
origin + Vector.up * 2.4
|
||||
else
|
||||
origin
|
||||
end
|
||||
end
|
||||
|
||||
3
assets/base/islands_terrain/manifest.yaml
Normal file
3
assets/base/islands_terrain/manifest.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: "islands_terrain"
|
||||
model: "islands_terrain.obj"
|
||||
collision: "mesh"
|
||||
32
assets/base/islands_terrain/model/islands_terrain.mtl
Normal file
32
assets/base/islands_terrain/model/islands_terrain.mtl
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
5920
assets/base/islands_terrain/model/islands_terrain.obj
Normal file
5920
assets/base/islands_terrain/model/islands_terrain.obj
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
component(:building)
|
||||
|
||||
on.create do |event|
|
||||
map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(-1.5, 1.5, -4.52), Vector.new(0, 20, 0), data: {team: nil})
|
||||
map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(-1.5, 1.5, -4.52), Vector.new(0, 20, 0), data: { team: nil })
|
||||
map.insert_entity("base", "information_panel", event.entity.position + Vector.new(3, 0, 1), Vector.new(0, -90, 0))
|
||||
map.insert_entity("base", "door", event.entity.position + Vector.new(0, 0, 6), Vector.new(0, 0, 0))
|
||||
map.insert_entity("base", "door", event.entity.position + Vector.new(0, 0, 6), Vector.new(0, 180, 0))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
component(:vehicle) # Generic, Weapon
|
||||
|
||||
on.button_down(:interact) do |event|
|
||||
$window.console.stdin("#{event.entity.name} handled button_down(:interact)")
|
||||
CyberarmEngine::Window.instance.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)
|
||||
# event.player.exit_vehicle
|
||||
# end
|
||||
end
|
||||
end
|
||||
|
||||
22
assets/base/shared/sound/master.json
Normal file
22
assets/base/shared/sound/master.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
BIN
assets/base/shared/sound/sfx/shield_regen.wav
Normal file
BIN
assets/base/shared/sound/sfx/shield_regen.wav
Normal file
Binary file not shown.
@@ -1,11 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
component(:building)
|
||||
|
||||
on.create do |event|
|
||||
map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(6, 1.5, 3), Vector.new(0, -90, 0), data: {team: nil})
|
||||
map.insert_entity("base", "purchase_terminal", event.entity.position + Vector.new(6, 1.5, 3), Vector.new(0, -90, 0), data: { team: nil })
|
||||
map.insert_entity("base", "information_panel", event.entity.position + Vector.new(0.5, 0, 3), Vector.new(0, 90, 0))
|
||||
map.insert_entity("base", "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"]))
|
||||
end
|
||||
# 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
|
||||
|
||||
BIN
blends/islands_terrain.blend
Normal file
BIN
blends/islands_terrain.blend
Normal file
Binary file not shown.
@@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
IMICFPS_SERVER_MODE = true
|
||||
require_relative "i-mic-fps"
|
||||
|
||||
director = IMICFPS::Networking::Director.new(mode: :server, hostname: "0.0.0.0", port: 56789, interface: IMICFPS::Networking::MemoryServer)
|
||||
director = IMICFPS::Networking::Director.new(mode: :server, hostname: "0.0.0.0", port: 56_789, interface: IMICFPS::Networking::MemoryServer)
|
||||
director.define_singleton_method(:tick) do |dt|
|
||||
puts "Ticked: #{dt}"
|
||||
end
|
||||
|
||||
director.run.join
|
||||
director.run.join
|
||||
|
||||
166
i-mic-fps.rb
166
i-mic-fps.rb
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "fiddle"
|
||||
require "yaml"
|
||||
require "json"
|
||||
@@ -5,133 +7,58 @@ require "abbrev"
|
||||
require "time"
|
||||
require "socket"
|
||||
require "tmpdir"
|
||||
require "securerandom"
|
||||
|
||||
require "opengl"
|
||||
require "glu"
|
||||
require "nokogiri"
|
||||
require "async/websocket"
|
||||
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"
|
||||
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
|
||||
|
||||
require_relative "lib/version"
|
||||
require_relative "lib/constants"
|
||||
require_relative "lib/common_methods"
|
||||
def require_all(directory)
|
||||
files = Dir["#{directory}/**/*.rb"].sort!
|
||||
file_order = []
|
||||
|
||||
require_relative "lib/trees/aabb_tree_debug"
|
||||
require_relative "lib/trees/aabb_tree"
|
||||
require_relative "lib/trees/aabb_node"
|
||||
loop do
|
||||
failed = []
|
||||
first_name_error = nil
|
||||
|
||||
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"
|
||||
files.each do |file|
|
||||
begin
|
||||
require_relative file
|
||||
file_order << file
|
||||
rescue NameError => e
|
||||
failed << file
|
||||
first_name_error ||= e
|
||||
end
|
||||
end
|
||||
|
||||
require_relative "lib/renderer/renderer"
|
||||
require_relative "lib/renderer/g_buffer"
|
||||
require_relative "lib/renderer/opengl_renderer"
|
||||
require_relative "lib/renderer/bounding_box_renderer"
|
||||
if failed.size == files.size
|
||||
raise first_name_error
|
||||
else
|
||||
files = failed
|
||||
end
|
||||
break if failed.empty?
|
||||
end
|
||||
|
||||
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
|
||||
# pp file_order.map { |f| f.gsub(".rb", "")}
|
||||
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/multiplayer_menu"
|
||||
require_relative "lib/ui/menus/level_select_menu"
|
||||
require_relative "lib/ui/menus/game_pause_menu"
|
||||
|
||||
require_relative "lib/states/game_states/boot"
|
||||
require_relative "lib/states/game_states/close"
|
||||
require_relative "lib/states/game_states/game"
|
||||
require_relative "lib/states/game_states/loading_state"
|
||||
|
||||
require_relative "lib/hud"
|
||||
require_relative "lib/hud/widget"
|
||||
require_relative "lib/hud/widgets/ammo"
|
||||
require_relative "lib/hud/widgets/radar"
|
||||
require_relative "lib/hud/widgets/health"
|
||||
|
||||
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_relative "lib/scripting"
|
||||
require_relative "lib/scripting/sandbox"
|
||||
require_relative "lib/scripting/whitelist"
|
||||
|
||||
require_relative "lib/component"
|
||||
require_relative "lib/components/building"
|
||||
|
||||
require_relative "lib/game_objects/entity"
|
||||
require_relative "lib/game_objects/light"
|
||||
require_relative "lib/game_objects/particle_emitter"
|
||||
|
||||
require_relative "lib/game_objects/camera"
|
||||
require_relative "lib/game_objects/entities/player"
|
||||
require_relative "lib/game_objects/entities/skydome"
|
||||
require_relative "lib/game_objects/entities/terrain"
|
||||
|
||||
require_relative "lib/texture"
|
||||
require_relative "lib/model"
|
||||
require_relative "lib/model_cache"
|
||||
require_relative "lib/model/parser"
|
||||
require_relative "lib/model/model_object"
|
||||
require_relative "lib/model/material"
|
||||
|
||||
require_relative "lib/model/parsers/wavefront_parser"
|
||||
require_relative "lib/model/parsers/collada_parser"
|
||||
|
||||
require_relative "lib/map_parser"
|
||||
require_relative "lib/manifest"
|
||||
require_relative "lib/map"
|
||||
|
||||
require_relative "lib/scene"
|
||||
require_relative "lib/scenes/turn_table"
|
||||
|
||||
require_relative "lib/crosshair"
|
||||
require_relative "lib/demo"
|
||||
|
||||
require_relative "lib/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/overlay"
|
||||
require_relative "lib/window"
|
||||
|
||||
require_relative "lib/tools/asset_viewer"
|
||||
require_relative "lib/tools/map_editor"
|
||||
require_all "lib"
|
||||
|
||||
# Don't launch game if IMICFPS_SERVER_MODE is defined
|
||||
# or if game is being packaged
|
||||
@@ -147,24 +74,41 @@ def prevent_launch?
|
||||
return [true, "#{m}: Packaging lockfile is present (#{packaging_lockfile})"]
|
||||
end
|
||||
|
||||
return [false, ""]
|
||||
[false, ""]
|
||||
end
|
||||
|
||||
unless prevent_launch?[0]
|
||||
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
|
||||
|
||||
window = IMICFPS::Window.new(
|
||||
width: window_width,
|
||||
height: window_height,
|
||||
fullscreen: window_fullscreen,
|
||||
resizable: !window_fullscreen,
|
||||
update_interval: 1000.0 / fps_target
|
||||
)
|
||||
|
||||
if ARGV.join.include?("--profile")
|
||||
begin
|
||||
require "ruby-prof"
|
||||
RubyProf.start
|
||||
IMICFPS::Window.new.show
|
||||
|
||||
window.show
|
||||
|
||||
result = RubyProf.stop
|
||||
printer = RubyProf::MultiPrinter.new(result)
|
||||
printer.print(path: ".", profile: "profile", min_percent: 2)
|
||||
rescue LoadError
|
||||
puts "ruby-prof not installed!"
|
||||
raise
|
||||
end
|
||||
else
|
||||
IMICFPS::Window.new.show
|
||||
window.show
|
||||
end
|
||||
else
|
||||
puts prevent_launch?[1]
|
||||
end
|
||||
end
|
||||
|
||||
149
lib/camera_controller.rb
Normal file
149
lib/camera_controller.rb
Normal file
@@ -0,0 +1,149 @@
|
||||
# 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
|
||||
@@ -1,19 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
def self.assets_path
|
||||
File.expand_path("./../../assets", __FILE__)
|
||||
File.expand_path("../assets", __dir__)
|
||||
end
|
||||
|
||||
module CommonMethods
|
||||
def window
|
||||
CyberarmEngine::Window.instance
|
||||
end
|
||||
|
||||
def window; $window; end
|
||||
def delta_time
|
||||
window.delta_time
|
||||
end
|
||||
|
||||
def delta_time; (Gosu.milliseconds - window.delta_time) / 1000.0; end
|
||||
def button_down?(id); window.button_down?(id); 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
|
||||
@@ -24,32 +43,43 @@ class IMICFPS
|
||||
def formatted_number(number)
|
||||
string = number.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
|
||||
|
||||
string.insert(0, "-") if number < 0
|
||||
string.insert(0, "-") if number.negative?
|
||||
|
||||
return string
|
||||
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)
|
||||
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,
|
||||
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)
|
||||
@@ -87,14 +117,5 @@ class IMICFPS
|
||||
-2
|
||||
)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Component
|
||||
COMPONENTS = {}
|
||||
@components = {}
|
||||
|
||||
def self.get(name)
|
||||
COMPONENTS.dig(name)
|
||||
@components[name]
|
||||
end
|
||||
|
||||
def self.inherited(subclass)
|
||||
COMPONENTS["__pending"] ||= []
|
||||
COMPONENTS["__pending"] << subclass
|
||||
@components["__pending"] ||= []
|
||||
@components["__pending"] << subclass
|
||||
end
|
||||
|
||||
def self.initiate
|
||||
return unless COMPONENTS.dig("__pending") # Already setup
|
||||
return unless @components["__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
|
||||
@@ -30,12 +32,10 @@ class IMICFPS
|
||||
string = self.class.name.split("::").last
|
||||
split = string.scan(/[A-Z][a-z]*/)
|
||||
|
||||
component_name = "#{split.map { |s| s.downcase }.join("_")}".to_sym
|
||||
|
||||
return component_name
|
||||
split.map(&:downcase).join("_").to_s.to_sym
|
||||
end
|
||||
|
||||
def setup
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Components
|
||||
class Building < Component
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
GAME_ROOT_PATH = File.expand_path("..", File.dirname(__FILE__))
|
||||
|
||||
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)
|
||||
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"
|
||||
|
||||
# Objects exported from blender using the default or meter object scale will be close to 1 GL unit
|
||||
MODEL_METER_SCALE = 1.0
|
||||
@@ -13,4 +14,4 @@ class IMICFPS
|
||||
EARTH_GRAVITY = 9.8 # m/s
|
||||
# Moon
|
||||
MOON_GRAVITY = 1.625 # m/s
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Crosshair
|
||||
include CommonMethods
|
||||
|
||||
def initialize(color: Gosu::Color.rgb(255,127,0), size: 10, thickness: 3)
|
||||
def initialize(color: Gosu::Color.rgb(255, 127, 0), size: 10, thickness: 3)
|
||||
@color = color
|
||||
@size = size
|
||||
@thickness = thickness
|
||||
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
|
||||
end
|
||||
|
||||
36
lib/demo.rb
36
lib/demo.rb
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Demo
|
||||
def initialize(camera:, player:, demo:, mode:)
|
||||
@@ -6,7 +8,7 @@ class IMICFPS
|
||||
@demo = demo
|
||||
@mode = mode
|
||||
|
||||
@index= 0
|
||||
@index = 0
|
||||
@tick = 0
|
||||
@changed = false
|
||||
|
||||
@@ -30,7 +32,7 @@ class IMICFPS
|
||||
@file.puts("tick #{@index}")
|
||||
end
|
||||
|
||||
@file.puts("down #{InputMapper.action(id)}")
|
||||
@file.puts("down #{InputMapper.actions(id)}")
|
||||
@changed = true
|
||||
end
|
||||
end
|
||||
@@ -42,7 +44,7 @@ class IMICFPS
|
||||
@file.puts("tick #{@index}")
|
||||
end
|
||||
|
||||
@file.puts("up #{InputMapper.action(id)}")
|
||||
@file.puts("up #{InputMapper.actions(id)}")
|
||||
@changed = true
|
||||
end
|
||||
end
|
||||
@@ -54,33 +56,37 @@ 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
|
||||
@index += 1
|
||||
|
||||
until(@data[@index]&.start_with?("tick"))
|
||||
until @data[@index]&.start_with?("tick")
|
||||
break unless @data[@index]
|
||||
|
||||
data = @data[@index].split(" ")
|
||||
if data.first == "up"
|
||||
case data.first
|
||||
when "up"
|
||||
input = InputMapper.get(data.last.to_sym)
|
||||
key = input.is_a?(Array) ? input.first : input
|
||||
$window.current_state.button_up(key) if key
|
||||
CyberarmEngine::Window.instance.current_state.button_up(key) if key
|
||||
|
||||
elsif data.first == "down"
|
||||
when "down"
|
||||
input = InputMapper.get(data.last.to_sym)
|
||||
key = input.is_a?(Array) ? input.first : input
|
||||
$window.current_state.button_down(key) if key
|
||||
CyberarmEngine::Window.instance.current_state.button_down(key) if key
|
||||
|
||||
elsif data.first == "mouse"
|
||||
when "mouse"
|
||||
@camera.orientation.z = data[1].to_f
|
||||
@player.orientation.y = (data[2].to_f * -1) - 180
|
||||
else
|
||||
# hmm
|
||||
end
|
||||
|
||||
@index += 1
|
||||
@@ -105,4 +111,4 @@ class IMICFPS
|
||||
@index += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class EventHandler
|
||||
class Event
|
||||
attr_reader :entity, :context
|
||||
|
||||
def initialize(entity:, context: nil)
|
||||
@entity, @context = entity, context
|
||||
@entity = entity
|
||||
@context = context
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class EventHandler
|
||||
@@handlers = {}
|
||||
@@ -22,7 +24,7 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def self.get(event)
|
||||
@@handlers.dig(event)
|
||||
@@handlers[event]
|
||||
end
|
||||
|
||||
def initialize
|
||||
@@ -36,4 +38,4 @@ class IMICFPS
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class EventHandler
|
||||
class EntityLifeCycle < EventHandler
|
||||
def handles
|
||||
[:create, :move, :destroy]
|
||||
%i[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)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class EventHandler
|
||||
class EntityMoved < EventHandler
|
||||
@@ -5,11 +7,11 @@ 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)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class EventHandler
|
||||
class Input < EventHandler
|
||||
def handles
|
||||
[:button_down, :button_up]
|
||||
%i[button_down button_up]
|
||||
end
|
||||
|
||||
def handle(subscriber, context, *args)
|
||||
@@ -13,12 +15,10 @@ class IMICFPS
|
||||
|
||||
if action.is_a?(Numeric) && action == key
|
||||
subscriber.trigger(event)
|
||||
else
|
||||
if InputMapper.get(action) == key
|
||||
subscriber.trigger(event)
|
||||
end
|
||||
elsif InputMapper.get(action) == key
|
||||
subscriber.trigger(event)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
13
lib/ext/element.rb
Normal file
13
lib/ext/element.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
case OpenGL.get_platform
|
||||
when :OPENGL_PLATFORM_WINDOWS
|
||||
OpenGL.load_lib("opengl32.dll", "C:/Windows/System32")
|
||||
@@ -28,8 +30,8 @@ when :OPENGL_PLATFORM_LINUX
|
||||
OpenGL.load_lib("libGL.so", gl_library_path)
|
||||
GLU.load_lib("libGLU.so", gl_library_path)
|
||||
else
|
||||
raise RuntimeError, "Couldn't find GL libraries"
|
||||
raise "Couldn't find GL libraries"
|
||||
end
|
||||
else
|
||||
raise RuntimeError, "Unsupported platform."
|
||||
end
|
||||
raise "Unsupported platform."
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
if RUBY_VERSION < "2.5.0"
|
||||
puts "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
|
||||
puts "|NOTICE| Ruby is #{RUBY_VERSION} not 2.5.0+..............................|Notice|"
|
||||
@@ -11,8 +13,8 @@ if RUBY_VERSION < "2.5.0"
|
||||
elsif self > max
|
||||
max
|
||||
else
|
||||
return self
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
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
|
||||
83
lib/game_objects/entities/editor.rb
Normal file
83
lib/game_objects/entities/editor.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
# 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
|
||||
@@ -1,76 +1,20 @@
|
||||
require "etc"
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Player < Entity
|
||||
|
||||
attr_accessor :speed
|
||||
attr_reader :name, :bound_model, :first_person_view
|
||||
attr_reader :name, :bound_model
|
||||
|
||||
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
|
||||
@@ -117,19 +61,9 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def jump
|
||||
if InputMapper.down?(:jump) && window.current_state.map.collision_manager.on_ground?(self)
|
||||
@velocity.y = 1.5
|
||||
end
|
||||
end
|
||||
return unless InputMapper.down?(:jump) && window.director.map.collision_manager.on_ground?(self)
|
||||
|
||||
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
|
||||
@velocity.y = 1.5
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Skydome < Entity
|
||||
def setup
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Terrain < Entity
|
||||
end
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
|
||||
|
||||
# A game object is any renderable thing
|
||||
class Entity
|
||||
include CommonMethods
|
||||
|
||||
attr_accessor :visible, :renderable, :backface_culling
|
||||
attr_accessor :position, :orientation, :scale, :velocity
|
||||
attr_reader :name, :debug_color, :bounding_box, :drag, :camera, :manifest
|
||||
attr_accessor :visible, :renderable, :backface_culling, :position, :orientation, :scale, :velocity, :debug_color
|
||||
attr_reader :name, :bounding_box, :drag, :camera, :manifest, :model
|
||||
|
||||
def initialize(manifest:, map_entity: nil, spawnpoint: nil, backface_culling: true, run_scripts: true)
|
||||
@manifest = manifest
|
||||
@@ -16,7 +15,7 @@ class IMICFPS
|
||||
@position = map_entity.position.clone
|
||||
@orientation = map_entity.orientation.clone
|
||||
@scale = map_entity.scale.clone
|
||||
@bound_model = bind_model
|
||||
bind_model
|
||||
elsif spawnpoint
|
||||
@position = spawnpoint.position.clone
|
||||
@orientation = spawnpoint.orientation.clone
|
||||
@@ -45,8 +44,7 @@ class IMICFPS
|
||||
|
||||
setup
|
||||
|
||||
if @bound_model
|
||||
@bound_model.model.entity = self
|
||||
if @model
|
||||
@normalized_bounding_box = normalize_bounding_box_with_offset
|
||||
|
||||
normalize_bounding_box
|
||||
@@ -54,7 +52,7 @@ class IMICFPS
|
||||
|
||||
@camera = nil
|
||||
|
||||
return self
|
||||
self
|
||||
end
|
||||
|
||||
def load_scripts
|
||||
@@ -68,22 +66,15 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def bind_model
|
||||
model = ModelCache.new(manifest: @manifest)
|
||||
model = ModelCache.find_or_cache(manifest: @manifest)
|
||||
raise "model isn't a model!" unless model.is_a?(Model)
|
||||
|
||||
raise "model isn't a model!" unless model.is_a?(ModelCache)
|
||||
@bound_model = model
|
||||
@bound_model.model.entity = self
|
||||
@model = model
|
||||
@bounding_box = normalize_bounding_box_with_offset
|
||||
|
||||
return model
|
||||
end
|
||||
|
||||
def model
|
||||
@bound_model.model if @bound_model
|
||||
end
|
||||
|
||||
def unbind_model
|
||||
@bound_model = nil
|
||||
@model = nil
|
||||
end
|
||||
|
||||
def attach_camera(camera)
|
||||
@@ -101,10 +92,7 @@ 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
|
||||
@@ -113,20 +101,16 @@ 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
|
||||
@bound_model.model.bounding_box.normalize_with_offset(self)
|
||||
@model.bounding_box.normalize_with_offset(self)
|
||||
end
|
||||
|
||||
def normalize_bounding_box
|
||||
@bound_model.model.bounding_box.normalize(self)
|
||||
@model.bounding_box.normalize(self)
|
||||
end
|
||||
|
||||
def model_matrix
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
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
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
def initialize(position:, image:, interval: 1_500, time_to_live: 3_000, max_particles: 500)
|
||||
end
|
||||
@@ -7,4 +9,4 @@ class IMICFPS
|
||||
|
||||
def update
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
23
lib/hud.rb
23
lib/hud.rb
@@ -1,14 +1,27 @@
|
||||
# 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
|
||||
|
||||
@@ -19,5 +32,13 @@ class IMICFPS
|
||||
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
|
||||
end
|
||||
|
||||
@@ -1,13 +1,50 @@
|
||||
# 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]
|
||||
@margin = 10
|
||||
|
||||
setup
|
||||
end
|
||||
@@ -20,6 +57,20 @@ class IMICFPS
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class HUD
|
||||
class AmmoWidget < HUD::Widget
|
||||
def setup
|
||||
@text = Text.new("")
|
||||
@background = Gosu::Color.new(0x88222222)
|
||||
@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 - @margin, @text.y - @margin,
|
||||
@text.width + @margin * 2, @text.height + @margin * 2,
|
||||
@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
|
||||
@@ -17,13 +19,13 @@ class IMICFPS
|
||||
|
||||
def update
|
||||
if (Gosu.milliseconds / 1000.0) % 1.0 >= 0.9
|
||||
random = "#{rand(0..999)}".rjust(3, "0")
|
||||
@text.text = "Pistol\nAMMO: #{random}"
|
||||
random = rand(0..199).to_s.rjust(3, "0")
|
||||
@text.text = "#{random}/999"
|
||||
end
|
||||
|
||||
@text.x = window.width - (@margin * 2 + @text.width)
|
||||
@text.y = window.height - (@margin * 2 + @text.height)
|
||||
@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
|
||||
end
|
||||
|
||||
97
lib/hud/widgets/chat.rb
Normal file
97
lib/hud/widgets/chat.rb
Normal file
@@ -0,0 +1,97 @@
|
||||
# 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
|
||||
75
lib/hud/widgets/chat_history.rb
Normal file
75
lib/hud/widgets/chat_history.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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
|
||||
30
lib/hud/widgets/crosshair.rb
Normal file
30
lib/hud/widgets/crosshair.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# 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
|
||||
@@ -1,15 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class HUD
|
||||
class HealthWidget < HUD::Widget
|
||||
def setup
|
||||
@spacer = 0
|
||||
@text = Text.new("")
|
||||
@text = Text.new("", font: MONOSPACE_FONT, border: true, border_color: Gosu::Color::BLACK)
|
||||
@width = 512
|
||||
@height = 24
|
||||
@slant = 32
|
||||
|
||||
@color = Gosu::Color.rgba(100, 100, 200, 128)
|
||||
@shield = Gosu::Color.rgba(200, 100, 50, 200)
|
||||
@color = Gosu::Color.new(0x66ffa348)
|
||||
@shield = Gosu::Color.new(0xaae66100)
|
||||
|
||||
@health = 0.0
|
||||
end
|
||||
@@ -17,32 +19,35 @@ class IMICFPS
|
||||
def draw
|
||||
@text.draw
|
||||
fill_quad(
|
||||
window.width / 2 - @width / 2, @spacer, # TOP LEFT
|
||||
window.width / 2 + @width / 2, @spacer, # TOP RIGHT
|
||||
window.width / 2 + @width / 2 - @slant, @spacer + @height, # BOTTOM RIGHT
|
||||
window.width / 2 - @width / 2 + @slant, @spacer + @height, # BOTTOM LEFT
|
||||
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, # TOP LEFT
|
||||
(window.width / 2 - @width / 2) + @width * @health, @spacer, # TOP RIGHT
|
||||
(window.width / 2 - @width / 2) + @width * @health - @slant, @spacer + @height, # BOTTOM RIGHT
|
||||
window.width / 2 - @width / 2 + @slant, @spacer + @height, # BOTTOM LEFT
|
||||
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}".rjust(3, "0")
|
||||
percentage = (@health * 100).round.to_s.rjust(3, "0")
|
||||
@text.text = "[Health #{percentage}%]"
|
||||
@text.x = window.width / 2 - @text.width / 2
|
||||
@text.y = @spacer + @height / 2 - @text.height / 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
|
||||
end
|
||||
|
||||
@@ -1,28 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class HUD
|
||||
class RadarWidget < HUD::Widget
|
||||
def setup
|
||||
@size = 256
|
||||
@color = Gosu::Color.new(0x88222222)
|
||||
@min_size = 148
|
||||
@max_size = 288
|
||||
@target_screen_width = 1920
|
||||
@size = @max_size
|
||||
|
||||
@text = Text.new("RADAR")
|
||||
@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(
|
||||
@margin, window.height - (@size + @margin),
|
||||
Widget.horizontal_margin, window.height - (@size + Widget.vertical_margin),
|
||||
@size, @size,
|
||||
@color
|
||||
@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
|
||||
@text.text = "RADAR: X #{@player.position.x.round(1)} Y #{@player.position.z.round(1)}"
|
||||
@text.x = @margin + @size / 2 - @text.width / 2
|
||||
@text.y = window.height - (@margin + @size)
|
||||
@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
|
||||
end
|
||||
|
||||
94
lib/hud/widgets/score_board.rb
Normal file
94
lib/hud/widgets/score_board.rb
Normal file
@@ -0,0 +1,94 @@
|
||||
# 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
|
||||
36
lib/hud/widgets/squad.rb
Normal file
36
lib/hud/widgets/squad.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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
|
||||
@@ -1,6 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class CollisionManager
|
||||
attr_reader :map, :collisions
|
||||
|
||||
def initialize(map:)
|
||||
@map = map
|
||||
@collisions = {}
|
||||
@@ -47,13 +50,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 > 0
|
||||
search.reject! {|ent| ent == entity || !ent.collidable?}
|
||||
if search.size.positive?
|
||||
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)
|
||||
@@ -72,15 +75,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 == 0 && target.orientation == 0
|
||||
return entity.bounding_box if target.position.zero? && target.orientation.zero?
|
||||
|
||||
# "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
|
||||
|
||||
return box
|
||||
box
|
||||
end
|
||||
|
||||
def on_ground?(entity) # TODO: Use some form of caching to speed this up
|
||||
@@ -95,7 +98,7 @@ class IMICFPS
|
||||
|
||||
broadphase.detect do |ent|
|
||||
ray = Ray.new(entity.position - ent.position, Vector.down)
|
||||
if ent.model.aabb_tree.search(ray).size > 0
|
||||
if ent.model.aabb_tree.search(ray).size.positive?
|
||||
on_ground = true
|
||||
return true
|
||||
end
|
||||
@@ -104,7 +107,7 @@ class IMICFPS
|
||||
break if on_ground
|
||||
end
|
||||
|
||||
return on_ground
|
||||
on_ground
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module EntityManager # Get included into GameState context
|
||||
def add_entity(entity)
|
||||
@collision_manager.add(entity) if @collision_manager && entity.manifest.collision# Add every entity to collision manager
|
||||
if @collision_manager && entity.manifest.collision
|
||||
@collision_manager.add(entity)
|
||||
end # Add every entity to collision manager
|
||||
Publisher.instance.publish(:create, nil, entity)
|
||||
@entities << entity
|
||||
end
|
||||
|
||||
def insert_entity(package, name, position, orientation, data = {})
|
||||
ent = MapParser::Entity.new(package, name, position, orientation, Vector.new(1,1,1))
|
||||
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 {|entity| entity == entity}
|
||||
@entities.detect { |e| e == entity }
|
||||
end
|
||||
|
||||
def find_entity_by(name:)
|
||||
@entities.detect { |entity| entity.name == name}
|
||||
@entities.detect { |entity| entity.name == name }
|
||||
end
|
||||
|
||||
def remove_entity(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
|
||||
return unless (ent = @entities.detect { |e| e == entity })
|
||||
|
||||
@collision_manager.remove(entity) if @collision_manager && entity.manifest.collision
|
||||
@publisher.publish(:destroy, nil, entity)
|
||||
@entities.delete(ent)
|
||||
end
|
||||
|
||||
def entities
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class InputMapper
|
||||
@@keymap = {}
|
||||
@@ -15,11 +17,12 @@ class IMICFPS
|
||||
if id_or_action.is_a?(Integer)
|
||||
@@keys[id_or_action] = true
|
||||
else
|
||||
query = @@keymap.dig(id_or_action)
|
||||
query = @@keymap[id_or_action]
|
||||
|
||||
if query.is_a?(Integer)
|
||||
query
|
||||
elsif query.is_a?(Array)
|
||||
case query
|
||||
when Integer
|
||||
query
|
||||
when Array
|
||||
query.each do |key|
|
||||
@@keys[key] = true
|
||||
end
|
||||
@@ -33,11 +36,12 @@ class IMICFPS
|
||||
if id_or_action.is_a?(Integer)
|
||||
@@keys[id_or_action] = false
|
||||
else
|
||||
query = @@keymap.dig(id_or_action)
|
||||
query = @@keymap[id_or_action]
|
||||
|
||||
if query.is_a?(Integer)
|
||||
case query
|
||||
when Integer
|
||||
query
|
||||
elsif query.is_a?(Array)
|
||||
when Array
|
||||
query.each do |key|
|
||||
@@keys[key] = false
|
||||
end
|
||||
@@ -48,12 +52,14 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def self.get(action)
|
||||
@@keymap.dig(action)
|
||||
@@keymap[action]
|
||||
end
|
||||
|
||||
def self.set(action, key)
|
||||
raise "action must be a symbol" unless action.is_a?(Symbol)
|
||||
raise "key must be a whole number or Array of whole numbers, got #{key}" unless key.is_a?(Integer) || key.is_a?(Array)
|
||||
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
|
||||
|
||||
warn "InputMapper.set(:#{action}) is already defined as #{@@keymap[action]}" if @@keymap[action]
|
||||
|
||||
@@ -73,7 +79,7 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def self.is?(action, query_key)
|
||||
keys = @@keymap.dig(action)
|
||||
keys = @@keymap[action]
|
||||
|
||||
if keys.is_a?(Array)
|
||||
keys.include?(query_key)
|
||||
@@ -82,23 +88,19 @@ class IMICFPS
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
def self.actions(key)
|
||||
@@keymap.select do |action, value|
|
||||
case value
|
||||
when Array
|
||||
action if value.include?(key)
|
||||
when key
|
||||
action
|
||||
end
|
||||
end
|
||||
|
||||
answer
|
||||
end.map { |keymap| keymap.first.is_a?(Symbol) ? keymap.first : keymap.first.first }
|
||||
end
|
||||
|
||||
def self.reset_keys
|
||||
@@keys.each do |key, value|
|
||||
@@keys.each do |key, _value|
|
||||
@@keys[key] = false
|
||||
end
|
||||
end
|
||||
@@ -129,4 +131,4 @@ IMICFPS::InputMapper.set(:decrease_mouse_sensitivity, Gosu::KB_NUMPAD_MINUS)
|
||||
IMICFPS::InputMapper.set(:reset_mouse_sensitivity, Gosu::KB_NUMPAD_MULTIPLY)
|
||||
|
||||
IMICFPS::InputMapper.set(:decrease_view_distance, Gosu::MsWheelDown)
|
||||
IMICFPS::InputMapper.set(:increase_view_distance, Gosu::MsWheelUp)
|
||||
IMICFPS::InputMapper.set(:increase_view_distance, Gosu::MsWheelUp)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module LightManager
|
||||
MAX_LIGHTS = OpenGL::GL_MAX_LIGHTS
|
||||
@@ -6,7 +8,7 @@ class IMICFPS
|
||||
@lights << model
|
||||
end
|
||||
|
||||
def find_light()
|
||||
def find_light
|
||||
end
|
||||
|
||||
def lights
|
||||
@@ -14,7 +16,7 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def light_count
|
||||
@lights.count+1
|
||||
@lights.count + 1
|
||||
end
|
||||
|
||||
def clear_lights
|
||||
@@ -23,7 +25,8 @@ 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 $window.config.get(:debug_options, :stats)
|
||||
|
||||
puts "OpenGL::GL_LIGHT#{light_count}" if CyberarmEngine::Window.instance.config.get(:debug_options, :stats)
|
||||
Object.const_get "OpenGL::GL_LIGHT#{light_count}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
class IMICFPS
|
||||
class NetworkManager
|
||||
MULTICAST_ADDRESS = "224.0.0.1"
|
||||
MULTICAST_PORT = 30_000
|
||||
|
||||
REMOTE_GAMEHUB = "i-mic.rubyclan.org"
|
||||
REMOTE_GAMEHUB_PORT = 98765
|
||||
|
||||
DEFAULT_SERVER_HOST = "0.0.0.0"
|
||||
DEFAULT_SERVER_PORT = 56789
|
||||
DEFAULT_SERVER_QUERY_PORT = 28900
|
||||
def initialize
|
||||
end
|
||||
|
||||
# https://github.com/jpignata/blog/blob/master/articles/multicast-in-ruby.md
|
||||
def 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 handle_lan_multicast
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class PhysicsManager
|
||||
def initialize(collision_manager:)
|
||||
@@ -15,7 +17,7 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def resolve(entity, other)
|
||||
entity.velocity.y = other.velocity.y if other.velocity.y < entity.velocity.y && entity.velocity.y < 0
|
||||
entity.velocity.y = other.velocity.y if other.velocity.y < entity.velocity.y && entity.velocity.y.negative?
|
||||
end
|
||||
|
||||
def simulate
|
||||
@@ -32,9 +34,9 @@ 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 < 0
|
||||
entity.velocity.y = 0 if entity.velocity.y.negative?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
111
lib/managers/sound_manager.rb
Normal file
111
lib/managers/sound_manager.rb
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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
|
||||
@@ -1,33 +1,35 @@
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
raise "No manifest found at: #{manifest_file}" unless File.exist?(manifest_file)
|
||||
|
||||
@file = manifest_file
|
||||
parse(manifest_file)
|
||||
end
|
||||
|
||||
def parse(file)
|
||||
data = YAML.load(File.read(file))
|
||||
data = YAML.safe_load(File.read(file))
|
||||
|
||||
# required
|
||||
@name = data["name"]
|
||||
@model = data["model"]
|
||||
|
||||
# optional
|
||||
@collision = data["collision"] ? data["collision"] : nil
|
||||
@collision_mesh = data["collision_mesh"] ? data["collision_mesh"] : nil
|
||||
@collision = data["collision"] || nil
|
||||
@collision_mesh = data["collision_mesh"] || nil
|
||||
@collision_resolution = data["collision_resolution"] ? data["collision_resolution"].to_sym : :static
|
||||
@physics = data["physics"] ? data["physics"] : false
|
||||
@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
|
||||
@@ -39,7 +41,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
|
||||
@@ -47,7 +49,7 @@ class IMICFPS
|
||||
list << Script.new(script, File.read("#{path}.rb"))
|
||||
end
|
||||
|
||||
return list
|
||||
list
|
||||
end
|
||||
|
||||
def parse_dependencies(list)
|
||||
@@ -56,7 +58,7 @@ class IMICFPS
|
||||
dependencies << Dependency.new(item["package"], item["name"])
|
||||
end
|
||||
|
||||
return dependencies
|
||||
dependencies
|
||||
end
|
||||
|
||||
def file_path
|
||||
|
||||
100
lib/map.rb
100
lib/map.rb
@@ -1,11 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Map
|
||||
include EntityManager
|
||||
include LightManager
|
||||
include CommonMethods
|
||||
|
||||
attr_reader :collision_manager
|
||||
attr_reader :gravity
|
||||
attr_reader :collision_manager, :gravity
|
||||
|
||||
def initialize(map_parser:, gravity: IMICFPS::EARTH_GRAVITY)
|
||||
@map_parser = map_parser
|
||||
@gravity = gravity
|
||||
@@ -18,24 +20,92 @@ class IMICFPS
|
||||
end
|
||||
|
||||
def setup
|
||||
add_entity(Terrain.new(map_entity: @map_parser.terrain, manifest: Manifest.new(package: @map_parser.terrain.package, name: @map_parser.terrain.name)))
|
||||
add_terrain if @map_parser.terrain.name
|
||||
add_skybox if @map_parser.skydome.name
|
||||
add_lights
|
||||
add_entities
|
||||
|
||||
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))
|
||||
# TODO: Add player entity from director
|
||||
add_entity(
|
||||
Player.new(
|
||||
spawnpoint: @map_parser.spawnpoints.sample,
|
||||
manifest: Manifest.new(
|
||||
package: "base",
|
||||
name: "character"
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def add_terrain
|
||||
add_entity(
|
||||
Terrain.new(
|
||||
map_entity: @map_parser.terrain,
|
||||
manifest: Manifest.new(
|
||||
package: @map_parser.terrain.package,
|
||||
name: @map_parser.terrain.name
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def add_skybox
|
||||
add_entity(
|
||||
Skydome.new(
|
||||
map_entity: @map_parser.skydome,
|
||||
backface_culling: false,
|
||||
manifest: Manifest.new(
|
||||
package: @map_parser.skydome.package,
|
||||
name: @map_parser.skydome.name
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def add_lights
|
||||
@map_parser.lights.each do |l|
|
||||
add_light(Light.new(id: available_light, position: l.position, diffuse: l.diffuse, ambient: l.ambient, specular: l.specular, intensity: l.intensity))
|
||||
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
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@map_parser.entities.each do |ent|
|
||||
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")))
|
||||
|
||||
# Default lights if non are defined
|
||||
if @map_parser.lights.size == 0
|
||||
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)))
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,7 +118,7 @@ class IMICFPS
|
||||
|
||||
Gosu.gl do
|
||||
gl_error?
|
||||
glClearColor(0,0.2,0.5,1) # skyish blue
|
||||
glClearColor(0, 0.2, 0.5, 1) # skyish blue
|
||||
gl_error?
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) # clear the screen and the depth buffer
|
||||
gl_error?
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class MapParser
|
||||
attr_reader :metadata, :terrain, :skydome, :lights, :entities, :spawnpoints
|
||||
attr_reader :assets, :missing_assets
|
||||
attr_reader :metadata, :terrain, :skydome, :lights, :entities, :spawnpoints, :assets, :missing_assets
|
||||
|
||||
def initialize(map_file:)
|
||||
@metadata = MapParser::MetaData.new
|
||||
@terrain = MapParser::Entity.new
|
||||
@@ -16,6 +18,17 @@ 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))
|
||||
|
||||
@@ -27,7 +40,7 @@ class IMICFPS
|
||||
@metadata.thumbnail = section["thumbnail"] # TODO: convert thumbnail to Image
|
||||
@metadata.description = section["description"]
|
||||
else
|
||||
raise "Map metadata is missing!"
|
||||
warn "Map metadata is missing!"
|
||||
end
|
||||
|
||||
if section = data["terrain"]
|
||||
@@ -51,7 +64,7 @@ class IMICFPS
|
||||
end
|
||||
@terrain.water_level = section["water_level"]
|
||||
else
|
||||
raise "Map terrain data is missing!"
|
||||
warn "Map terrain data is missing!"
|
||||
end
|
||||
|
||||
if section = data["skydome"]
|
||||
@@ -74,13 +87,13 @@ class IMICFPS
|
||||
@skydome.scale = Vector.new(1, 1, 1)
|
||||
end
|
||||
else
|
||||
raise "Map skydome data is missing!"
|
||||
warn "Map skydome data is missing!"
|
||||
end
|
||||
|
||||
if section = data["lights"]
|
||||
section.each do |l|
|
||||
light = MapParser::Light.new
|
||||
light.type = IMICFPS::Light::POINT # TODO: fix me
|
||||
light.type = light_type(l["type"])
|
||||
light.position = Vector.new(
|
||||
l["position"]["x"],
|
||||
l["position"]["y"],
|
||||
@@ -137,7 +150,7 @@ class IMICFPS
|
||||
@entities << entity
|
||||
end
|
||||
else
|
||||
raise "Map has no entities!"
|
||||
warn "Map has no entities!"
|
||||
end
|
||||
|
||||
if section = data["spawnpoints"]
|
||||
@@ -158,7 +171,7 @@ class IMICFPS
|
||||
@spawnpoints << spawnpoint
|
||||
end
|
||||
else
|
||||
raise "Map has no spawnpoints!"
|
||||
warn "Map has no spawnpoints!"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
250
lib/model.rb
250
lib/model.rb
@@ -1,250 +0,0 @@
|
||||
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
|
||||
@@ -1,20 +0,0 @@
|
||||
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
|
||||
@@ -1,123 +0,0 @@
|
||||
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
|
||||
@@ -1,69 +0,0 @@
|
||||
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
|
||||
@@ -1,119 +0,0 @@
|
||||
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
|
||||
@@ -1,157 +0,0 @@
|
||||
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
|
||||
@@ -1,43 +0,0 @@
|
||||
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
|
||||
34
lib/networking.rb
Normal file
34
lib/networking.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
41
lib/networking/backend/README.md
Normal file
41
lib/networking/backend/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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
|
||||
```
|
||||
10
lib/networking/backend/channel.rb
Normal file
10
lib/networking/backend/channel.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module CyberarmEngine
|
||||
module Networking
|
||||
class Channel
|
||||
def initialize(id:, mode:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
82
lib/networking/backend/connection.rb
Normal file
82
lib/networking/backend/connection.rb
Normal file
@@ -0,0 +1,82 @@
|
||||
# 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
|
||||
32
lib/networking/backend/packet.rb
Normal file
32
lib/networking/backend/packet.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
140
lib/networking/backend/packet_handler.rb
Normal file
140
lib/networking/backend/packet_handler.rb
Normal file
@@ -0,0 +1,140 @@
|
||||
# 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
|
||||
34
lib/networking/backend/packets/control_packet.rb
Normal file
34
lib/networking/backend/packets/control_packet.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
0
lib/networking/backend/packets/frament_packet.rb
Normal file
0
lib/networking/backend/packets/frament_packet.rb
Normal file
32
lib/networking/backend/packets/raw_packet.rb
Normal file
32
lib/networking/backend/packets/raw_packet.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
34
lib/networking/backend/packets/reliable_packet.rb
Normal file
34
lib/networking/backend/packets/reliable_packet.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
45
lib/networking/backend/peer.rb
Normal file
45
lib/networking/backend/peer.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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
|
||||
31
lib/networking/backend/protocol.rb
Normal file
31
lib/networking/backend/protocol.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# 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
|
||||
186
lib/networking/backend/server.rb
Normal file
186
lib/networking/backend/server.rb
Normal file
@@ -0,0 +1,186 @@
|
||||
# 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
|
||||
@@ -1,6 +0,0 @@
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class MemoryConnection < Connection
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class MemoryServer < Server
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class Client
|
||||
def initialize(socket:)
|
||||
@socket = socket
|
||||
end
|
||||
|
||||
def read
|
||||
end
|
||||
|
||||
def write
|
||||
end
|
||||
|
||||
def close
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,17 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class Connection
|
||||
def initialize(hostname:, port:)
|
||||
attr_reader :address, :port
|
||||
attr_accessor :total_packets_sent, :total_packets_received, :total_data_sent, :total_data_received, :last_read_time, :last_write_time
|
||||
|
||||
def initialize(address:, port:)
|
||||
@address = address
|
||||
@port = port
|
||||
|
||||
@read_buffer = ReadBuffer.new
|
||||
@packet_write_queue = []
|
||||
|
||||
@peer_id = 0
|
||||
|
||||
@last_read_time = Networking.milliseconds
|
||||
@last_write_time = Networking.milliseconds
|
||||
|
||||
@total_packets_sent = 0
|
||||
@total_packets_received = 0
|
||||
@total_data_sent = 0
|
||||
@total_data_received = 0
|
||||
|
||||
@socket = nil
|
||||
end
|
||||
|
||||
def connect
|
||||
@socket = UDPSocket.new
|
||||
@socket.connect(@address, @port)
|
||||
|
||||
send_packet(
|
||||
Packet.new(
|
||||
peer_id: 0,
|
||||
sequence: 0,
|
||||
type: Protocol::CONNECT,
|
||||
payload: "Hello World!"
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def send_packet(packet)
|
||||
Packet.splinter(packet).each do |pkt|
|
||||
@packet_write_queue << pkt
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
while read
|
||||
end
|
||||
|
||||
write
|
||||
|
||||
# puts "#{Networking.milliseconds} Total sent: #{@total_packets_sent} packets, #{@total_data_sent} data"
|
||||
# puts "#{Networking.milliseconds} Total received: #{@total_packets_received} packets, #{@total_data_received} data"
|
||||
@read_buffer.reconstruct_packets.each do |packet, _addr_info|
|
||||
@peer_id = packet.payload.unpack1("C") if packet.peer_id.zero? && packet.type == Protocol::VERIFY_CONNECT
|
||||
end
|
||||
|
||||
if @peer_id.positive? && Networking.milliseconds - @last_read_time >= Protocol::HEARTBEAT_INTERVAL
|
||||
send_packet(Packet.new(peer_id: @peer_id, sequence: 0, type: Protocol::HEARTBEAT, payload: ""))
|
||||
end
|
||||
end
|
||||
|
||||
def close
|
||||
@socket&.close
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read
|
||||
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
|
||||
@read_buffer.add(data, addr)
|
||||
|
||||
@total_packets_received += 1
|
||||
@total_data_received += data.length
|
||||
@last_read_time = Networking.milliseconds
|
||||
true
|
||||
rescue IO::WaitReadable
|
||||
false
|
||||
end
|
||||
|
||||
def write
|
||||
while (packet = @packet_write_queue.shift)
|
||||
@socket.send(packet.encode, 0, @address, @port)
|
||||
|
||||
@total_data_sent += packet.encode.length
|
||||
@total_packets_sent += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,61 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class Director
|
||||
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
|
||||
attr_reader :tick_rate, :storage, :map, :server, :connection
|
||||
|
||||
def initialize(tick_rate: 15)
|
||||
@tick_rate = (1000.0 / tick_rate) / 1000.0
|
||||
|
||||
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
|
||||
@last_tick_time = CyberarmEngine::Networking.milliseconds
|
||||
@directing = true
|
||||
@storage = {}
|
||||
@map = nil
|
||||
end
|
||||
|
||||
def host_server(hostname: CyberarmEngine::Networking::DEFAULT_SERVER_HOSTNAME, port: CyberarmEngine::Networking::DEFAULT_SERVER_PORT, max_peers: CyberarmEngine::Networking::DEFAULT_PEER_LIMIT)
|
||||
@server = Server.new(hostname: hostname, port: port, max_peers: max_peers)
|
||||
@server.bind
|
||||
end
|
||||
|
||||
def connect(hostname:, port: CyberarmEngine::Networking::DEFAULT_SERVER_PORT)
|
||||
@connection = Connection.new(hostname: hostname, port: port)
|
||||
@connection.connect
|
||||
end
|
||||
|
||||
def load_map(map_parser:)
|
||||
# TODO: send map_change to clients
|
||||
@map = Map.new(map_parser: map_parser)
|
||||
@map.setup
|
||||
end
|
||||
|
||||
def run
|
||||
Thread.start do |thread|
|
||||
while(@directing)
|
||||
Thread.start do
|
||||
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(dt)
|
||||
def tick(delta_time)
|
||||
return unless @map
|
||||
|
||||
Publisher.instance.publish(:tick, delta_time)
|
||||
|
||||
@map.update
|
||||
@server&.update
|
||||
@connection&.update
|
||||
end
|
||||
|
||||
def shutdown
|
||||
@directing = false
|
||||
|
||||
@clients.each(&:close)
|
||||
@server.update if @server
|
||||
@connection.update if @connection
|
||||
end
|
||||
|
||||
def milliseconds
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
@server&.close
|
||||
@connection&.close
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module Networking
|
||||
module Events
|
||||
@@ -8,4 +10,4 @@ class IMICFPS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
@@ -1,6 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class PacketHandler
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
8
lib/networking/packet_handlers/snapshot.rb
Normal file
8
lib/networking/packet_handlers/snapshot.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class SnapshotPacket < CyberarmEngine::Networking::Packet
|
||||
end
|
||||
end
|
||||
end
|
||||
35
lib/networking/read_buffer.rb
Normal file
35
lib/networking/read_buffer.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
@@ -1,29 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
module Networking
|
||||
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
|
||||
class Server < CyberarmEngine::Networking::Server
|
||||
def connected(peer:)
|
||||
end
|
||||
|
||||
def bind
|
||||
def disconnected(peer:, reason:)
|
||||
end
|
||||
|
||||
def broadcast(packet)
|
||||
end
|
||||
|
||||
def update
|
||||
end
|
||||
|
||||
def close
|
||||
def packet_received(peer:, message:, channel:)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class IMICFPS
|
||||
class Overlay
|
||||
include CommonMethods
|
||||
@@ -5,19 +7,45 @@ class IMICFPS
|
||||
Slot = Struct.new(:value, :width)
|
||||
|
||||
def initialize
|
||||
@text = CyberarmEngine::Text.new("", x: 3, y: 3, shadow_color: Gosu::Color::BLACK)
|
||||
@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.width + 8
|
||||
|
||||
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
|
||||
|
||||
sample_points = 256
|
||||
frame_stats = CyberarmEngine::Stats.frames.select(&:complete?)
|
||||
return if frame_stats.empty?
|
||||
|
||||
right_origin = CyberarmEngine::Vector.new(10, 128)
|
||||
nodes = Array.new(sample_points) { [] }
|
||||
|
||||
slice = 0
|
||||
frame_stats.each_slice((CyberarmEngine::Stats.max_frame_history / sample_points.to_f).ceil) do |bucket|
|
||||
bucket.each do |frame|
|
||||
nodes[slice] << frame.frame_timing.duration
|
||||
end
|
||||
|
||||
slice += 1
|
||||
end
|
||||
|
||||
points = []
|
||||
nodes.each_with_index do |cluster, i|
|
||||
break if cluster.empty?
|
||||
|
||||
points << CyberarmEngine::Vector.new(right_origin.x + 1 * i, right_origin.y - cluster.max)
|
||||
end
|
||||
|
||||
Gosu.draw_path(points, Gosu::Color::WHITE, Float::INFINITY) if points.size > 1
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -29,12 +57,14 @@ class IMICFPS
|
||||
|
||||
if window.config.get(:options, :fps)
|
||||
create_slot "FPS: #{Gosu.fps}"
|
||||
create_slot "Frame time: #{Gosu.milliseconds - window.delta_time}ms" if window.config.get(:debug_options, :stats)
|
||||
if window.config.get(:debug_options, :stats)
|
||||
create_slot "Frame time: #{(window.delta_time * 1000).round.to_s}ms"
|
||||
end
|
||||
end
|
||||
|
||||
if window.config.get(:debug_options, :stats)
|
||||
create_slot "Vertices: #{formatted_number(window.number_of_vertices)}"
|
||||
create_slot "Faces: #{formatted_number(window.number_of_vertices / 3)}"
|
||||
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)
|
||||
@@ -48,7 +78,7 @@ class IMICFPS
|
||||
@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}" if i == @slots.size - 1
|
||||
@text.text += slot.value.to_s if i == @slots.size - 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,4 +86,4 @@ class IMICFPS
|
||||
@slots << Slot.new(string, @text.textobject.text_width(string))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# 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
|
||||
|
||||
@@ -21,13 +24,14 @@ 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.dig(event)
|
||||
if subscribers = @events[event]
|
||||
return unless event_handler = EventHandler.get(event)
|
||||
|
||||
subscribers.each do |subscriber|
|
||||
@@ -36,4 +40,4 @@ class IMICFPS
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
class IMICFPS
|
||||
class BoundingBoxRenderer
|
||||
attr_reader :bounding_boxes, :vertex_count
|
||||
def initialize(map:)
|
||||
@map = map
|
||||
|
||||
@bounding_boxes = {}
|
||||
@vertex_count = 0
|
||||
end
|
||||
|
||||
def create_bounding_box(object, box, color = nil, mesh_object_id)
|
||||
|
||||
color ||= object.debug_color
|
||||
|
||||
if @bounding_boxes[mesh_object_id]
|
||||
if @bounding_boxes[mesh_object_id][:color] != color
|
||||
@bounding_boxes[mesh_object_id][:colors] = mesh_colors(color).pack("f*")
|
||||
@bounding_boxes[mesh_object_id][:color] = color
|
||||
return
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
@bounding_boxes[mesh_object_id] = {object: object, box: box, color: color, objects: []}
|
||||
|
||||
box = object.normalize_bounding_box
|
||||
|
||||
normals = mesh_normals
|
||||
colors = mesh_colors(color)
|
||||
vertices = mesh_vertices(box)
|
||||
|
||||
@vertex_count+=vertices.size
|
||||
|
||||
@bounding_boxes[mesh_object_id][:vertices_size] = vertices.size
|
||||
@bounding_boxes[mesh_object_id][:vertices] = vertices.pack("f*")
|
||||
@bounding_boxes[mesh_object_id][:normals] = normals.pack("f*")
|
||||
@bounding_boxes[mesh_object_id][:colors] = colors.pack("f*")
|
||||
|
||||
object.model.objects.each do |mesh|
|
||||
data = {}
|
||||
box = mesh.bounding_box.normalize(object)
|
||||
|
||||
normals = mesh_normals
|
||||
colors = mesh_colors(mesh.debug_color)
|
||||
vertices = mesh_vertices(box)
|
||||
|
||||
@vertex_count+=vertices.size
|
||||
|
||||
data[:vertices_size] = vertices.size
|
||||
data[:vertices] = vertices.pack("f*")
|
||||
data[:normals] = normals.pack("f*")
|
||||
data[:colors] = colors.pack("f*")
|
||||
|
||||
@bounding_boxes[mesh_object_id][:objects] << data
|
||||
end
|
||||
end
|
||||
|
||||
def mesh_normals
|
||||
[
|
||||
0,1,0,
|
||||
0,1,0,
|
||||
0,1,0,
|
||||
0,1,0,
|
||||
0,1,0,
|
||||
0,1,0,
|
||||
|
||||
0,-1,0,
|
||||
0,-1,0,
|
||||
0,-1,0,
|
||||
0,-1,0,
|
||||
0,-1,0,
|
||||
0,-1,0,
|
||||
|
||||
0,0,1,
|
||||
0,0,1,
|
||||
0,0,1,
|
||||
0,0,1,
|
||||
0,0,1,
|
||||
0,0,1,
|
||||
|
||||
1,0,0,
|
||||
1,0,0,
|
||||
1,0,0,
|
||||
1,0,0,
|
||||
1,0,0,
|
||||
1,0,0,
|
||||
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
|
||||
-1,0,0,
|
||||
-1,0,0,
|
||||
-1,0,0
|
||||
]
|
||||
end
|
||||
|
||||
def mesh_colors(color)
|
||||
[
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue,
|
||||
color.red, color.green, color.blue
|
||||
]
|
||||
end
|
||||
|
||||
def mesh_vertices(box)
|
||||
[
|
||||
box.min.x, box.max.y, box.max.z,
|
||||
box.min.x, box.max.y, box.min.z,
|
||||
box.max.x, box.max.y, box.min.z,
|
||||
|
||||
box.min.x, box.max.y, box.max.z,
|
||||
box.max.x, box.max.y, box.max.z,
|
||||
box.max.x, box.max.y, box.min.z,
|
||||
|
||||
box.max.x, box.min.y, box.min.z,
|
||||
box.max.x, box.min.y, box.max.z,
|
||||
box.min.x, box.min.y, box.max.z,
|
||||
|
||||
box.max.x, box.min.y, box.min.z,
|
||||
box.min.x, box.min.y, box.min.z,
|
||||
box.min.x, box.min.y, box.max.z,
|
||||
|
||||
box.min.x, box.max.y, box.max.z,
|
||||
box.min.x, box.max.y, box.min.z,
|
||||
box.min.x, box.min.y, box.min.z,
|
||||
|
||||
box.min.x, box.min.y, box.max.z,
|
||||
box.min.x, box.min.y, box.min.z,
|
||||
box.min.x, box.max.y, box.max.z,
|
||||
|
||||
box.max.x, box.max.y, box.max.z,
|
||||
box.max.x, box.max.y, box.min.z,
|
||||
box.max.x, box.min.y, box.min.z,
|
||||
|
||||
box.max.x, box.min.y, box.max.z,
|
||||
box.max.x, box.min.y, box.min.z,
|
||||
box.max.x, box.max.y, box.max.z,
|
||||
|
||||
box.min.x, box.max.y, box.max.z,
|
||||
box.max.x, box.max.y, box.max.z,
|
||||
box.max.x, box.min.y, box.max.z,
|
||||
|
||||
box.min.x, box.max.y, box.max.z,
|
||||
box.max.x, box.min.y, box.max.z,
|
||||
box.min.x, box.min.y, box.max.z,
|
||||
|
||||
box.max.x, box.min.y, box.min.z,
|
||||
box.min.x, box.min.y, box.min.z,
|
||||
box.min.x, box.max.y, box.min.z,
|
||||
|
||||
box.max.x, box.min.y, box.min.z,
|
||||
box.min.x, box.max.y, box.min.z,
|
||||
box.max.x, box.max.y, box.min.z
|
||||
]
|
||||
end
|
||||
|
||||
def draw_bounding_boxes
|
||||
@bounding_boxes.each do |key, bounding_box|
|
||||
glPushMatrix
|
||||
|
||||
glTranslatef(bounding_box[:object].position.x, bounding_box[:object].position.y, bounding_box[:object].position.z)
|
||||
draw_bounding_box(bounding_box)
|
||||
@bounding_boxes[key][:objects].each {|o| draw_bounding_box(o)}
|
||||
|
||||
glPopMatrix
|
||||
|
||||
found = @map.entities.detect { |o| o == bounding_box[:object] }
|
||||
|
||||
unless found
|
||||
@vertex_count -= @bounding_boxes[key][:vertices_size]
|
||||
@bounding_boxes[key][:objects].each {|o| @vertex_count -= o[:vertices_size]}
|
||||
@bounding_boxes.delete(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def draw_bounding_box(bounding_box)
|
||||
glEnableClientState(GL_VERTEX_ARRAY)
|
||||
glEnableClientState(GL_COLOR_ARRAY)
|
||||
glEnableClientState(GL_NORMAL_ARRAY)
|
||||
|
||||
glVertexPointer(3, GL_FLOAT, 0, bounding_box[:vertices])
|
||||
glColorPointer(3, GL_FLOAT, 0, bounding_box[:colors])
|
||||
glNormalPointer(GL_FLOAT, 0, bounding_box[:normals])
|
||||
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
||||
glDisable(GL_LIGHTING)
|
||||
glDrawArrays(GL_TRIANGLES, 0, bounding_box[:vertices_size]/3)
|
||||
glEnable(GL_LIGHTING)
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
||||
|
||||
glDisableClientState(GL_VERTEX_ARRAY)
|
||||
glDisableClientState(GL_COLOR_ARRAY)
|
||||
glDisableClientState(GL_NORMAL_ARRAY)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,163 +0,0 @@
|
||||
class IMICFPS
|
||||
class GBuffer
|
||||
include CommonMethods
|
||||
|
||||
attr_reader :screen_vbo, :vertices, :uvs
|
||||
def initialize
|
||||
@framebuffer = nil
|
||||
@buffers = [:position, :diffuse, :normal, :texcoord]
|
||||
@textures = {}
|
||||
@screen_vbo = nil
|
||||
@ready = false
|
||||
|
||||
@vertices = [
|
||||
-1.0, -1.0, 0,
|
||||
1.0, -1.0, 0,
|
||||
-1.0, 1.0, 0,
|
||||
|
||||
-1.0, 1.0, 0,
|
||||
1.0, -1.0, 0,
|
||||
1.0, 1.0, 0,
|
||||
].freeze
|
||||
|
||||
@uvs = [
|
||||
0, 0,
|
||||
1, 0,
|
||||
0, 1,
|
||||
|
||||
0, 1,
|
||||
1, 0,
|
||||
1, 1
|
||||
].freeze
|
||||
|
||||
create_framebuffer
|
||||
create_screen_vbo
|
||||
end
|
||||
|
||||
def width
|
||||
window.width
|
||||
end
|
||||
|
||||
def height
|
||||
window.height
|
||||
end
|
||||
|
||||
def create_framebuffer
|
||||
buffer = ' ' * 4
|
||||
glGenFramebuffers(1, buffer)
|
||||
@framebuffer = buffer.unpack('L2').first
|
||||
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer)
|
||||
|
||||
create_textures
|
||||
|
||||
status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
|
||||
|
||||
if status != GL_FRAMEBUFFER_COMPLETE
|
||||
message = ""
|
||||
|
||||
case status
|
||||
when GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT
|
||||
message = "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT"
|
||||
when GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT
|
||||
message = "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT"
|
||||
when GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER
|
||||
message = "GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER"
|
||||
when GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER
|
||||
message = "GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER"
|
||||
when GL_FRAMEBUFFER_UNSUPPORTED
|
||||
message = "GL_FRAMEBUFFER_UNSUPPORTED"
|
||||
else
|
||||
message = "Unknown error!"
|
||||
end
|
||||
puts "Incomplete framebuffer: #{status}\nError: #{message}"
|
||||
else
|
||||
@ready = true
|
||||
end
|
||||
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
|
||||
end
|
||||
|
||||
def create_textures
|
||||
@buffers.size.times do |i|
|
||||
buffer = ' ' * 4
|
||||
glGenTextures(1, buffer)
|
||||
texture_id = buffer.unpack('L2').first
|
||||
@textures[@buffers[i]] = texture_id
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, texture_id)
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, nil)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, texture_id, 0)
|
||||
end
|
||||
|
||||
buffer = ' ' * 4
|
||||
glGenTextures(1, buffer)
|
||||
texture_id = buffer.unpack('L2').first
|
||||
@textures[:depth] = texture_id
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, texture_id)
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, nil)
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, texture_id, 0)
|
||||
|
||||
draw_buffers = [ GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3 ]
|
||||
glDrawBuffers(draw_buffers.size, draw_buffers.pack("I*"))
|
||||
end
|
||||
|
||||
def create_screen_vbo
|
||||
buffer = ' ' * 4
|
||||
glGenVertexArrays(1, buffer)
|
||||
@screen_vbo = buffer.unpack('L2').first
|
||||
|
||||
buffer = " " * 4
|
||||
glGenBuffers(1, buffer)
|
||||
@positions_buffer_id = buffer.unpack('L2').first
|
||||
|
||||
buffer = " " * 4
|
||||
glGenBuffers(1, buffer)
|
||||
@uvs_buffer_id = buffer.unpack('L2').first
|
||||
|
||||
glBindVertexArray(@screen_vbo)
|
||||
glBindBuffer(GL_ARRAY_BUFFER, @positions_buffer_id)
|
||||
glBufferData(GL_ARRAY_BUFFER, @vertices.size * Fiddle::SIZEOF_FLOAT, @vertices.pack("f*"), GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, nil)
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, @uvs_buffer_id)
|
||||
glBufferData(GL_ARRAY_BUFFER, @uvs.size * Fiddle::SIZEOF_FLOAT, @uvs.pack("f*"), GL_STATIC_DRAW);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, nil)
|
||||
|
||||
glBindVertexArray(0)
|
||||
end
|
||||
|
||||
def bind_for_writing
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, @framebuffer)
|
||||
end
|
||||
|
||||
def bind_for_reading
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, @framebuffer)
|
||||
end
|
||||
|
||||
def set_read_buffer(buffer)
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0 + @textures.keys.index(buffer))
|
||||
end
|
||||
|
||||
def unbind_framebuffer
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0)
|
||||
end
|
||||
|
||||
def texture(type)
|
||||
@textures[type]
|
||||
end
|
||||
|
||||
def clean_up
|
||||
glDeleteFramebuffers(1, [@framebuffer].pack("L"))
|
||||
|
||||
glDeleteTextures(@textures.values.size, @textures.values.pack("L*"))
|
||||
|
||||
glDeleteBuffers(2, [@positions_buffer_id, @uvs_buffer_id].pack("L*"))
|
||||
glDeleteVertexArrays(1, [@screen_vbo].pack("L"))
|
||||
gl_error?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,246 +0,0 @@
|
||||
class IMICFPS
|
||||
class OpenGLRenderer
|
||||
include CommonMethods
|
||||
|
||||
@@immediate_mode_warning = false
|
||||
|
||||
def initialize
|
||||
@g_buffer = GBuffer.new
|
||||
end
|
||||
|
||||
def canvas_size_changed
|
||||
@g_buffer.unbind_framebuffer
|
||||
@g_buffer.clean_up
|
||||
|
||||
@g_buffer = GBuffer.new
|
||||
end
|
||||
|
||||
def render(camera, lights, entities)
|
||||
if window.config.get(:debug_options, :use_shaders) && Shader.available?("default") && Shader.available?("render_screen")
|
||||
@g_buffer.bind_for_writing
|
||||
gl_error?
|
||||
|
||||
glClearColor(0.0, 0.0, 0.0, 0.0)
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
Shader.use("default") do |shader|
|
||||
lights.each_with_index do |light, i|
|
||||
shader.uniform_float("lights[#{i}.end", -1.0);
|
||||
shader.uniform_float("lights[#{i}.type", light.type);
|
||||
shader.uniform_vec3("lights[#{i}].position", light.position)
|
||||
shader.uniform_vec3("lights[#{i}].ambient", light.ambient)
|
||||
shader.uniform_vec3("lights[#{i}].diffuse", light.diffuse)
|
||||
shader.uniform_vec3("lights[#{i}].specular", light.specular)
|
||||
end
|
||||
gl_error?
|
||||
|
||||
|
||||
shader.uniform_integer("totalLights", lights.size)
|
||||
|
||||
entities.each do |entity|
|
||||
next unless entity.visible && entity.renderable
|
||||
|
||||
shader.uniform_transform("projection", camera.projection_matrix)
|
||||
shader.uniform_transform("view", camera.view_matrix)
|
||||
shader.uniform_transform("model", entity.model_matrix)
|
||||
shader.uniform_boolean("hasTexture", entity.model.has_texture?)
|
||||
shader.uniform_vec3("cameraPosition", camera.position)
|
||||
|
||||
gl_error?
|
||||
draw_model(entity.model, shader)
|
||||
entity.draw
|
||||
end
|
||||
end
|
||||
|
||||
@g_buffer.unbind_framebuffer
|
||||
gl_error?
|
||||
|
||||
|
||||
@g_buffer.bind_for_reading
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0)
|
||||
|
||||
# lighting(lights)
|
||||
post_processing
|
||||
render_framebuffer
|
||||
|
||||
@g_buffer.unbind_framebuffer
|
||||
gl_error?
|
||||
else
|
||||
puts "Shader 'default' failed to compile, using immediate mode for rendering..." unless @@immediate_mode_warning
|
||||
@@immediate_mode_warning = true
|
||||
|
||||
gl_error?
|
||||
lights.each(&:draw)
|
||||
camera.draw
|
||||
|
||||
glEnable(GL_NORMALIZE)
|
||||
entities.each do |entity|
|
||||
next unless entity.visible && entity.renderable
|
||||
|
||||
glPushMatrix
|
||||
|
||||
glTranslatef(entity.position.x, entity.position.y, entity.position.z)
|
||||
glScalef(entity.scale.x, entity.scale.y, entity.scale.z)
|
||||
glRotatef(entity.orientation.x, 1.0, 0, 0)
|
||||
glRotatef(entity.orientation.y, 0, 1.0, 0)
|
||||
glRotatef(entity.orientation.z, 0, 0, 1.0)
|
||||
|
||||
gl_error?
|
||||
draw_mesh(entity.model)
|
||||
entity.draw
|
||||
glPopMatrix
|
||||
end
|
||||
end
|
||||
|
||||
gl_error?
|
||||
end
|
||||
|
||||
def lighting(lights)
|
||||
@g_buffer.set_read_buffer(:position)
|
||||
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
|
||||
0, 0, @g_buffer.width / 2, @g_buffer.height / 2,
|
||||
GL_COLOR_BUFFER_BIT, GL_LINEAR)
|
||||
|
||||
@g_buffer.set_read_buffer(:diffuse)
|
||||
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
|
||||
0, @g_buffer.height / 2, @g_buffer.width / 2, @g_buffer.height,
|
||||
GL_COLOR_BUFFER_BIT, GL_LINEAR)
|
||||
|
||||
@g_buffer.set_read_buffer(:normal)
|
||||
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
|
||||
@g_buffer.width / 2, @g_buffer.height / 2, @g_buffer.width, @g_buffer.height,
|
||||
GL_COLOR_BUFFER_BIT, GL_LINEAR)
|
||||
|
||||
@g_buffer.set_read_buffer(:texcoord)
|
||||
glBlitFramebuffer(0, 0, @g_buffer.width, @g_buffer.height,
|
||||
@g_buffer.width / 2, 0, @g_buffer.width, @g_buffer.height / 2,
|
||||
GL_COLOR_BUFFER_BIT, GL_LINEAR)
|
||||
end
|
||||
|
||||
def post_processing
|
||||
end
|
||||
|
||||
def render_framebuffer
|
||||
if Shader.available?("render_screen")
|
||||
Shader.use("render_screen") do |shader|
|
||||
glBindVertexArray(@g_buffer.screen_vbo)
|
||||
glEnableVertexAttribArray(0)
|
||||
glEnableVertexAttribArray(1)
|
||||
|
||||
glDisable(GL_DEPTH_TEST)
|
||||
glEnable(GL_BLEND)
|
||||
|
||||
glActiveTexture(GL_TEXTURE0)
|
||||
glBindTexture(GL_TEXTURE_2D, @g_buffer.texture(:diffuse))
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, @g_buffer.vertices.size)
|
||||
|
||||
glDisableVertexAttribArray(1)
|
||||
glDisableVertexAttribArray(0)
|
||||
glBindVertexArray(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def draw_model(model, shader)
|
||||
glBindVertexArray(model.vertex_array_id)
|
||||
glEnableVertexAttribArray(0)
|
||||
glEnableVertexAttribArray(1)
|
||||
glEnableVertexAttribArray(2)
|
||||
if model.has_texture?
|
||||
glBindTexture(GL_TEXTURE_2D, model.materials[model.textured_material].texture_id)
|
||||
|
||||
glEnableVertexAttribArray(3)
|
||||
glEnableVertexAttribArray(4)
|
||||
end
|
||||
|
||||
if window.config.get(:debug_options, :wireframe)
|
||||
glLineWidth(2)
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
||||
Shader.active_shader.uniform_boolean("disableLighting", true)
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, model.faces.count * 3)
|
||||
window.number_of_vertices += model.faces.count * 3
|
||||
|
||||
Shader.active_shader.uniform_boolean("disableLighting", false)
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
||||
glLineWidth(1)
|
||||
end
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, model.faces.count * 3)
|
||||
window.number_of_vertices += model.faces.count * 3
|
||||
|
||||
if model.has_texture?
|
||||
glDisableVertexAttribArray(4)
|
||||
glDisableVertexAttribArray(3)
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0)
|
||||
end
|
||||
glDisableVertexAttribArray(2)
|
||||
glDisableVertexAttribArray(1)
|
||||
glDisableVertexAttribArray(0)
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
||||
glBindVertexArray(0)
|
||||
end
|
||||
|
||||
def draw_mesh(model)
|
||||
model.objects.each_with_index do |o, i|
|
||||
glEnable(GL_CULL_FACE) if model.entity.backface_culling
|
||||
glEnable(GL_COLOR_MATERIAL)
|
||||
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
|
||||
glShadeModel(GL_FLAT) unless o.faces.first[4]
|
||||
glShadeModel(GL_SMOOTH) if o.faces.first[4]
|
||||
glEnableClientState(GL_VERTEX_ARRAY)
|
||||
glEnableClientState(GL_COLOR_ARRAY)
|
||||
glEnableClientState(GL_NORMAL_ARRAY)
|
||||
|
||||
if model.has_texture?
|
||||
glEnable(GL_TEXTURE_2D)
|
||||
glBindTexture(GL_TEXTURE_2D, model.materials[model.textured_material].texture_id)
|
||||
glEnableClientState(GL_TEXTURE_COORD_ARRAY)
|
||||
glTexCoordPointer(3, GL_FLOAT, 0, o.flattened_textures)
|
||||
end
|
||||
|
||||
glVertexPointer(4, GL_FLOAT, 0, o.flattened_vertices)
|
||||
glColorPointer(3, GL_FLOAT, 0, o.flattened_materials)
|
||||
glNormalPointer(GL_FLOAT, 0, o.flattened_normals)
|
||||
|
||||
if window.config.get(:debug_options, :wireframe) # This is kinda expensive
|
||||
glDisable(GL_LIGHTING)
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
|
||||
glPolygonOffset(2, 0.5)
|
||||
glLineWidth(3)
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4)
|
||||
window.number_of_vertices+=model.vertices.size
|
||||
|
||||
glLineWidth(1)
|
||||
glPolygonOffset(0, 0)
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
|
||||
glEnable(GL_LIGHTING)
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4)
|
||||
window.number_of_vertices+=model.vertices.size
|
||||
else
|
||||
glDrawArrays(GL_TRIANGLES, 0, o.flattened_vertices_size/4)
|
||||
window.number_of_vertices+=model.vertices.size
|
||||
end
|
||||
|
||||
# glBindBuffer(GL_ARRAY_BUFFER, 0)
|
||||
|
||||
glDisableClientState(GL_VERTEX_ARRAY)
|
||||
glDisableClientState(GL_COLOR_ARRAY)
|
||||
glDisableClientState(GL_NORMAL_ARRAY)
|
||||
|
||||
if model.has_texture?
|
||||
glDisableClientState(GL_TEXTURE_COORD_ARRAY)
|
||||
glDisable(GL_TEXTURE_2D)
|
||||
end
|
||||
|
||||
glDisable(GL_CULL_FACE) if model.entity.backface_culling
|
||||
glDisable(GL_COLOR_MATERIAL)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
class IMICFPS
|
||||
class Renderer
|
||||
include CommonMethods
|
||||
|
||||
attr_reader :opengl_renderer, :bounding_box_renderer
|
||||
|
||||
def initialize
|
||||
# @bounding_box_renderer = BoundingBoxRenderer.new(map: map)
|
||||
@opengl_renderer = OpenGLRenderer.new
|
||||
end
|
||||
|
||||
def preload_default_shaders
|
||||
shaders = ["default", "render_screen"]
|
||||
shaders.each do |shader|
|
||||
Shader.new(
|
||||
name: shader,
|
||||
includes_dir: "shaders/include",
|
||||
vertex: "shaders/vertex/#{shader}.glsl",
|
||||
fragment: "shaders/fragment/#{shader}.glsl"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def draw(camera, lights, entities)
|
||||
glViewport(0, 0, window.width, window.height)
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
|
||||
@opengl_renderer.render(camera, lights, entities)
|
||||
end
|
||||
|
||||
def canvas_size_changed
|
||||
@opengl_renderer.canvas_size_changed
|
||||
end
|
||||
|
||||
def finalize # cleanup
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user