79 Commits

Author SHA1 Message Date
dependabot[bot]
4c3c549f27 Bump nokogiri from 1.14.0.rc1 to 1.14.3
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.14.0.rc1 to 1.14.3.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.14.0.rc1...v1.14.3)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-20 21:15:06 +00:00
2da9edb6d0 Refactored to use CyberarmEngine::Window#delta_time instead of our reimplementing it, add WIP frame timing to Overlay using CyberarmEngine::Stats.frames data 2023-04-20 16:14:29 -05:00
8f2d7ff905 Moved shaders to cyberarm_engine, patched up settings menu, updated gems. 2023-01-08 17:29:31 -06:00
580c9d79ce Updated gems, fix issues caused by the removal of $window from cyberarm_engine 2022-07-16 06:59:47 -05:00
07c8dfa45b Merge pull request #11 from cyberarm/dependabot/bundler/nokogiri-1.13.6
Bump nokogiri from 1.13.4 to 1.13.6
2022-05-24 09:37:58 -05:00
dependabot[bot]
b69bdc8e46 Bump nokogiri from 1.13.4 to 1.13.6
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.4 to 1.13.6.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.4...v1.13.6)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-24 05:21:15 +00:00
3481de2498 Merge pull request #9 from cyberarm/dependabot/bundler/nokogiri-1.13.4
Bump nokogiri from 1.13.2 to 1.13.4
2022-04-12 08:06:00 -05:00
dependabot[bot]
7bcd973a88 Bump nokogiri from 1.13.2 to 1.13.4
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.13.2 to 1.13.4.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/v1.13.4/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.13.2...v1.13.4)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-12 10:23:14 +00:00
1d34cbb63d Merge pull request #8 from cyberarm/dependabot/bundler/nokogiri-1.13.2
Bump nokogiri from 1.11.6 to 1.13.2
2022-03-12 21:27:21 -06:00
dependabot[bot]
2df2df8488 Bump nokogiri from 1.11.6 to 1.13.2
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.11.6 to 1.13.2.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.11.6...v1.13.2)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-26 06:56:08 +00:00
87bff4ab82 Extracted Console into CyberarmEngine 2021-06-26 13:10:54 +00:00
cf37b94d80 Initial music playlist implementation, added menu music 2021-06-17 23:54:46 +00:00
0fe1d85924 Added Window#input_hijack to allow objects to get exclusive access to button_down/up callbacks, improved Chat widget 2021-06-03 15:14:06 +00:00
ec2b32ff92 Added title screen sound, added sound when mousing over menu links, update "required" Ruby version to 3.0+ in README 2021-06-03 03:05:10 +00:00
3986b1b0af Tweak menus a bit, add proper single player menu, made back link have some margin from above links 2021-06-03 00:59:08 +00:00
58b2f8b890 Added vertical_/horizontal_margin/padding to Widgets, added HUD command to toggle HUD 2021-06-02 12:55:08 +00:00
5cb48233fb Removed vertices count from Window, use new CyberarmEngine::Renderer::OpenGLRenderer#vertices_count for debug stats 2021-05-30 14:09:32 +00:00
da54bf5c53 Replaced usages of Text shadow with properly named border, use new correct Text shadow for menu titles, use text border for a few more elements 2021-05-29 00:36:47 +00:00
3570a80d67 Added icons for settings menu, refreshed menus to unify layouts 2021-05-27 02:39:32 +00:00
51363d2e3d Changed HUD text to have solid black 'border' 2021-05-26 23:18:52 +00:00
a16e14b0e5 Merge pull request #6 from cyberarm/dependabot/bundler/nokogiri-1.11.4
Bump nokogiri from 1.11.1 to 1.11.4
2021-05-20 15:05:59 +00:00
dependabot[bot]
4ebc4772a1 Bump nokogiri from 1.11.1 to 1.11.4
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.11.1 to 1.11.4.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.11.1...v1.11.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-20 15:00:13 +00:00
15ba3fb15b Fixed AssetViewer crashing when loading a scripted asset 2021-04-25 23:22:01 +00:00
c5e0f33f21 Addded some life to the Close state with some animations 2021-04-25 15:38:39 +00:00
d3fbf5dcf5 Breathe a little life into the Boot state 2021-04-25 14:57:03 +00:00
5b662f83cf Fixed g_buffer shader failing to compile 2021-04-24 20:09:21 +00:00
55bfe6ed79 Clean up window initialization a little bit, prevent window from being resizable if fullscreen: 'fixes' gosu believing that the window is smaller than it is 2021-04-19 19:11:01 +00:00
bd414cf765 Removed border from links 2021-03-29 08:52:08 -05:00
c848a11c12 WIP changes sync 2021-03-29 08:52:08 -05:00
5759055838 Added initial gamma correction 2021-03-29 08:14:29 -05:00
f04217ccc7 Fixed map editor state crashing 2020-12-14 16:04:12 -06:00
5ebcc56c33 Update README.md 2020-12-09 18:32:05 -06:00
8420ccd364 Unfreeze SoundManager caches 2020-12-02 17:40:51 -06:00
95bea199ed Ran rubocop autocorrect 2020-12-02 17:37:48 -06:00
aa30ff73d0 Rubocop autocorrect frozen string literal cop 2020-12-02 17:29:01 -06:00
791351f2f5 Replaced the Nobile font with Cantarell, added a BOLD_SANS_FONT constant 2020-12-02 17:25:48 -06:00
9aa5dc7174 Cleanup, moved Map lifecycle into Director, added renderer_info command 2020-12-02 11:38:10 -06:00
c7590366a6 Initial work on locales, more work on netcode 2020-12-01 13:02:22 -06:00
b9c30ade80 More work on networking code 2020-11-30 20:09:26 -06:00
ecbbc77ca7 Added rubocop config, more work on CyberarmEngine Netcode; basic sending and receiving of packets is now functional 2020-11-30 15:41:37 -06:00
85ec285263 Tweaked menus, stubbed multiplayer profile menu, updated readme 2020-11-28 09:36:39 -06:00
4c8e6c3d5f update settings menu, misc tweaks and fixes 2020-09-24 10:45:15 -05:00
9558370ab9 Updated excon gem, disabled netcode in Game to able game to be "playable" 2020-08-01 11:18:53 -05:00
e09bd06d24 BROKEN: Work on netcode refactor 2020-08-01 08:06:13 -05:00
87b4b8ef92 Update readme 2020-07-21 08:08:17 -05:00
2a36c58abe Stubbed files for simplified networking system 2020-07-20 21:36:56 -05:00
e69dd3402d Updated in-game screenshot 2020-07-20 19:56:07 -05:00
dd7a7ac602 Update README.md 2020-07-20 19:16:53 -05:00
985f7c331f Updated gems, added test.rake rakefile that includes default rake task 2020-07-20 19:06:30 -05:00
b6c64b4b6b Update ruby.yml 2020-07-20 19:00:14 -05:00
d91398b529 Update ruby.yml 2020-07-20 18:58:30 -05:00
f7945bf47d Create ruby.yml 2020-07-20 18:54:27 -05:00
d72e8ccbd0 Misc. 2020-07-19 09:42:55 -05:00
4ee97cca4b Use fonts everywhere, misc. tweaks 2020-07-18 21:33:33 -05:00
ce90284001 Storm Sync 2020-07-18 18:20:24 -05:00
1d7cd19b41 Added more hud widgets for showing squadmates and crosshair, added crosshair image and source svg, hackish fix to make renderer resize on windows size change, added CameraController camera control is back in 😂 2020-07-18 15:54:00 -05:00
f6e4a509fd Added fonts and tweaked hud, added chat history and score board hud widgets 2020-07-18 13:00:22 -05:00
b4a0a7a8bc Update README.md 2020-07-15 21:38:57 -05:00
65cfc1a124 Moved renderering system and model loading into CyberarmEngine, added island_test_map 2020-07-15 21:29:18 -05:00
9264ef6e58 Initial reimplementation of shader based lighting 2020-07-10 07:19:09 -05:00
635f4e3720 Updated MapEditor menu 2020-07-09 06:57:42 -05:00
5827f0b907 Added initial faction emblems 2020-07-08 11:22:22 -05:00
bac0311263 Added prototype multiplayer lobby 2020-05-16 08:11:34 -05:00
94c3dea7b9 Console now will resize when window is resized 2020-05-16 07:10:10 -05:00
fbdea30015 Patched bounding box renderer to work again- in immediate mode renderer, more work on lighting rework for modern renderer 2020-05-14 09:54:08 -05:00
dae950c72a Restructured deferred lighting/rendering 2020-05-12 15:19:05 -05:00
7c81dd93e3 Apply matrix to vertices parsed from collada models 2020-05-12 09:32:00 -05:00
fa2873bb8f Fixed model with texture and texture-less objects having texture-less objects renderer 'black' due to having hasTexture incorrectly set for shader 2020-05-11 23:05:34 -05:00
f72a8d4c35 Added support for renderering multiple textures per Model 2020-05-11 22:49:25 -05:00
d03c3ffcd8 Fixed Networking.milliseconds not returning milliseconds, fixed ReadBuffer not deleting processed buffers, misc. 2020-05-11 08:56:16 -05:00
644c1916b2 Packets can be sent/received now 2020-05-10 20:30:22 -05:00
a0f8ce4bfb Added default texture, refactored Texture to cache textures which have a file path 2020-05-10 17:57:46 -05:00
cf1e72225c More work on implementing networking 2020-05-10 11:05:16 -05:00
e94f2582f9 More fleshing out of networking 2020-05-09 09:56:03 -05:00
bae6a6a332 Removed async-websocket, more work on netcode 2020-05-08 21:42:14 -05:00
e3a2c9abe0 Added require_all method to replace explicitly requiring every source file, added SoundManager and sound effects, added sound for shield/health regen 2020-05-08 19:03:45 -05:00
bc695df4a1 Refactored ModelCache into a module, misc. fixes due to change. 2020-05-07 21:45:18 -05:00
bc480f9fae Fixed min opengl warning showing for opengl >3.3 2020-05-07 13:37:11 -05:00
d212691b71 tweaks to release.rake 2020-05-07 13:37:11 -05:00
189 changed files with 11158 additions and 3710 deletions

37
.github/workflows/ruby.yml vendored Normal file
View 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
View File

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

14
Gemfile
View File

@@ -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

View File

@@ -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

View File

@@ -1,17 +1,46 @@
![Ruby](https://github.com/cyberarm/i-mic-fps/workflows/Ruby/badge.svg)
![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/cyberarm/i-mic-fps?include_prereleases)
![GitHub repo size](https://img.shields.io/github/repo-size/cyberarm/i-mic-fps)
# I-MIC FPS
![logo](https://raw.githubusercontent.com/cyberarm/i-mic-fps/master/svg/logo.svg)
Creating a multiplayer first-person-shooter in pure Ruby; Using C extensions only for Rendering, Sound, and Input. ([Gosu](https://libgosu.org) and [opengl-bindings](https://github.com/vaiorabbit/ruby-opengl/))
![](https://raw.githubusercontent.com/cyberarm/i-mic-fps/master/screenshots/screenshot-game.png)
![screenshot](https://raw.githubusercontent.com/cyberarm/i-mic-fps/master/screenshots/screenshot-game.png)
## 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)*

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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

View 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.

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
class IMICFPS
class Components
class Building < Component
end
end
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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
View 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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View 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
```

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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