8 Commits

7 changed files with 320 additions and 19 deletions

View File

@@ -1,8 +1,13 @@
# CyberarmEngine # CyberarmEngine
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/cyberarm_engine`. To experiment with that code, run `bin/console` for an interactive prompt. Yet Another Game Engine On Top Of Gosu
TODO: Delete this and the text above, and describe your gem ## Features
* [Shoes-like](http://shoesrb.com) GUI support
* OpenGL Shader support (requires [opengl-bindings](https://github.com/vaiorabbit/ruby-opengl) gem)
* Includes classes for handling Vectors, Rays, Bounding Boxes, and Transforms
* GameState system
* Monolithic GameObjects
## Installation ## Installation
@@ -22,7 +27,32 @@ Or install it yourself as:
## Usage ## Usage
TODO: Write usage instructions here ```ruby
require "cyberarm_engine"
class Hello < CyberarmEngine::GuiState
def setup
stack do
label "Hello World!"
button "close" do
window.close
end
end
end
end
class Window < CyberarmEngine::Engine
def initialize
super
self.show_cursor = true
push_state(Hello)
end
end
Window.new.show
```
## Development ## Development

View File

@@ -4,6 +4,7 @@ rescue LoadError => e
pp e pp e
require "gosu" require "gosu"
end end
require "json"
require_relative "cyberarm_engine/version" require_relative "cyberarm_engine/version"
@@ -24,6 +25,7 @@ require_relative "cyberarm_engine/animator"
require_relative "cyberarm_engine/text" require_relative "cyberarm_engine/text"
require_relative "cyberarm_engine/timer" require_relative "cyberarm_engine/timer"
require_relative "cyberarm_engine/config_file"
require_relative "cyberarm_engine/ui/dsl" require_relative "cyberarm_engine/ui/dsl"

View File

@@ -0,0 +1,46 @@
module CyberarmEngine
class ConfigFile
def initialize(file:)
@file = file
if File.exist?(@file)
deserialize
else
@data = {}
end
end
def []= *keys, value
last_key = keys.last
if keys.size == 1
hash = @data
else
keys.pop
hash = @data[keys.shift] ||= {}
keys.each do |key|
hash[key] ||= {}
end
end
hash[last_key] = value
end
def get(*keys)
@data.dig(*keys)
end
def serialize
JSON.dump(@data)
end
def deserialize
@data = JSON.parse(File.read(@file), symbolize_names: true)
end
def save!
File.open(@file, "w") { |f| f.write(serialize) }
end
end
end

View File

@@ -3,7 +3,7 @@ module CyberarmEngine
include Common include Common
attr_accessor :options, :global_pause attr_accessor :options, :global_pause
attr_reader :game_objects, :containers attr_reader :game_objects
def initialize(options={}) def initialize(options={})
@options = options @options = options

View File

@@ -2,13 +2,44 @@ module CyberarmEngine
# Ref: https://github.com/vaiorabbit/ruby-opengl/blob/master/sample/OrangeBook/brick.rb # Ref: https://github.com/vaiorabbit/ruby-opengl/blob/master/sample/OrangeBook/brick.rb
class Shader class Shader
include OpenGL include OpenGL
@@shaders = {} @@shaders = {} # Cache for {Shader} instances
PREPROCESSOR_CHARACTER = "@" PREPROCESSOR_CHARACTER = "@".freeze # magic character for preprocessor phase of {Shader} compilation
# add instance of {Shader} to cache
#
# @param name [String]
# @param instance [Shader]
def self.add(name, instance) def self.add(name, instance)
@@shaders[name] = instance @@shaders[name] = instance
end end
# removes {Shader} from cache and cleans up
#
# @param name [String]
def self.delete(name)
shader = @@shaders.dig(name)
if shader
@@shaders.delete(name)
if shader.compiled?
glDeleteProgram(shader.program)
end
end
end
##
# runs _block_ using {Shader} with _name_
#
# @example
#
# CyberarmEngine::Shader.use("blur") do |shader|
# shader.uniform_float("radius", 20.0)
# # OpenGL Code that uses shader
# end
#
# @param name [String] name of {Shader} to use
# @return [void]
def self.use(name, &block) def self.use(name, &block)
shader = @@shaders.dig(name) shader = @@shaders.dig(name)
if shader if shader
@@ -18,22 +49,37 @@ module CyberarmEngine
end end
end end
# returns whether {Shader} with _name_ is in cache
#
# @param name [String]
# @return [Boolean]
def self.available?(name) def self.available?(name)
@@shaders.dig(name).is_a?(Shader) @@shaders.dig(name).is_a?(Shader)
end end
# returns instance of {Shader}, if it exists
#
# @param name [String]
# @return [Shader?]
def self.get(name) def self.get(name)
@@shaders.dig(name) @@shaders.dig(name)
end end
# returns currently active {Shader}, if one is active
#
# @return [Shader?]
def self.active_shader def self.active_shader
@active_shader @active_shader
end end
# sets currently active {Shader}
#
# @param instance [Shader] instance of {Shader} to set as active
def self.active_shader=(instance) def self.active_shader=(instance)
@active_shader = instance @active_shader = instance
end end
# stops using currently active {Shader}
def self.stop def self.stop
shader = Shader.active_shader shader = Shader.active_shader
@@ -44,11 +90,18 @@ module CyberarmEngine
end end
end end
# returns location of OpenGL Shader uniform
#
# @param variable [String]
def self.attribute_location(variable) def self.attribute_location(variable)
raise RuntimeError, "No active shader!" unless Shader.active_shader raise RuntimeError, "No active shader!" unless Shader.active_shader
Shader.active_shader.attribute_location(variable) Shader.active_shader.attribute_location(variable)
end end
# sets _variable_ to _value_
#
# @param variable [String]
# @param value
def self.set_uniform(variable, value) def self.set_uniform(variable, value)
raise RuntimeError, "No active shader!" unless Shader.active_shader raise RuntimeError, "No active shader!" unless Shader.active_shader
Shader.active_shader.set_uniform(variable, value) Shader.active_shader.set_uniform(variable, value)
@@ -64,7 +117,7 @@ module CyberarmEngine
@program = nil @program = nil
@error_buffer_size = 1024 @error_buffer_size = 1024 * 8
@variable_missing = {} @variable_missing = {}
@data = {shaders: {}} @data = {shaders: {}}
@@ -80,20 +133,30 @@ module CyberarmEngine
compile_shader(type: :fragment) compile_shader(type: :fragment)
link_shaders link_shaders
@data[:shaders].each { |key, id| glDeleteShader(id) }
# Only add shader if it successfully compiles # Only add shader if it successfully compiles
if @compiled if @compiled
puts "compiled!" puts "compiled!"
puts "Compiled shader: #{@name}" puts "Compiled shader: #{@name}"
Shader.add(@name, self) Shader.add(@name, self)
else else
glDeleteProgram(@program)
warn "FAILED to compile shader: #{@name}", "" warn "FAILED to compile shader: #{@name}", ""
end end
end end
# whether vertex and fragment files exist on disk
#
# @return [Boolean]
def shader_files_exist?(vertex:, fragment:) def shader_files_exist?(vertex:, fragment:)
File.exist?(vertex) && File.exist?(fragment) File.exist?(vertex) && File.exist?(fragment)
end end
# creates an OpenGL Shader of _type_ using _source_
#
# @param type [Symbol] valid values are: :vertex, :fragment
# @param source [String] source code for shader
def create_shader(type:, source:) def create_shader(type:, source:)
_shader = nil _shader = nil
@@ -103,7 +166,7 @@ module CyberarmEngine
when :fragment when :fragment
_shader = glCreateShader(GL_FRAGMENT_SHADER) _shader = glCreateShader(GL_FRAGMENT_SHADER)
else else
warn "Unsupported shader type: #{type.inspect}" raise ArgumentError, "Unsupported shader type: #{type.inspect}"
end end
processed_source = preprocess_source(source: source) processed_source = preprocess_source(source: source)
@@ -115,6 +178,23 @@ module CyberarmEngine
@data[:shaders][type] =_shader @data[:shaders][type] =_shader
end end
# evaluates shader preprocessors
#
# currently supported preprocessors:
#
# @include "file/path" "another/file/path" # => Replace line with contents of file; Shader includes_dir must be specified in constructor
#
# @example
# # Example Vertex Shader #
# # #version 330 core
# # @include "material_struct"
# # void main() {
# # gl_Position = vec4(1, 1, 1, 1);
# # }
#
# Shader.new(name: "model_renderer", includes_dir: "path/to/includes", vertex: "path/to/vertex_shader.glsl")
#
# @param source shader source code
def preprocess_source(source:) def preprocess_source(source:)
lines = source.lines lines = source.lines
@@ -141,6 +221,9 @@ module CyberarmEngine
lines.join lines.join
end end
# compile OpenGL Shader of _type_
#
# @return [Boolean] whether compilation succeeded
def compile_shader(type:) def compile_shader(type:)
_compiled = false _compiled = false
_shader = @data[:shaders][type] _shader = @data[:shaders][type]
@@ -166,6 +249,11 @@ module CyberarmEngine
return _compiled return _compiled
end end
# link compiled OpenGL Shaders in to a OpenGL Program
#
# @note linking must succeed or shader cannot be used
#
# @return [Boolean] whether linking succeeded
def link_shaders def link_shaders
@program = glCreateProgram @program = glCreateProgram
@data[:shaders].values.each do |_shader| @data[:shaders].values.each do |_shader|
@@ -187,7 +275,10 @@ module CyberarmEngine
@compiled = linked == 0 ? false : true @compiled = linked == 0 ? false : true
end end
# Returns the location of a uniform variable # Returns the location of a uniform _variable_
#
# @param variable [String]
# @return [Integer] location of uniform
def variable(variable) def variable(variable)
loc = glGetUniformLocation(@program, variable) loc = glGetUniformLocation(@program, variable)
if (loc == -1) if (loc == -1)
@@ -197,6 +288,7 @@ module CyberarmEngine
return loc return loc
end end
# @see Shader.use Shader.use
def use(&block) def use(&block)
return unless compiled? return unless compiled?
raise "Another shader is already in use! #{Shader.active_shader.name.inspect}" if Shader.active_shader raise "Another shader is already in use! #{Shader.active_shader.name.inspect}" if Shader.active_shader
@@ -210,49 +302,93 @@ module CyberarmEngine
end end
end end
# stop using shader, if shader is active
def stop def stop
Shader.active_shader = nil if Shader.active_shader == self Shader.active_shader = nil if Shader.active_shader == self
glUseProgram(0) glUseProgram(0)
end end
# @return [Boolean] whether {Shader} successfully compiled
def compiled? def compiled?
@compiled @compiled
end end
# returns location of a uniform _variable_
#
# @note Use {#variable} for friendly error handling
# @see #variable Shader#variable
#
# @param variable [String]
# @return [Integer]
def attribute_location(variable) def attribute_location(variable)
glGetUniformLocation(@program, variable) glGetUniformLocation(@program, variable)
end end
# send {Transform} to {Shader}
#
# @param variable [String]
# @param value [Transform]
# @param location [Integer]
# @return [void]
def uniform_transform(variable, value, location = nil) def uniform_transform(variable, value, location = nil)
attr_loc = location ? location : attribute_location(variable) attr_loc = location ? location : attribute_location(variable)
glUniformMatrix4fv(attr_loc, 1, GL_FALSE, value.to_gl.pack("F16")) glUniformMatrix4fv(attr_loc, 1, GL_FALSE, value.to_gl.pack("F16"))
end end
# send Boolean to {Shader}
#
# @param variable [String]
# @param value [Boolean]
# @param location [Integer]
# @return [void]
def uniform_boolean(variable, value, location = nil) def uniform_boolean(variable, value, location = nil)
attr_loc = location ? location : attribute_location(variable) attr_loc = location ? location : attribute_location(variable)
glUniform1i(attr_loc, value ? 1 : 0) glUniform1i(attr_loc, value ? 1 : 0)
end end
# send Integer to {Shader}
# @param variable [String]
# @param value [Integer]
# @param location [Integer]
# @return [void]
def uniform_integer(variable, value, location = nil) def uniform_integer(variable, value, location = nil)
attr_loc = location ? location : attribute_location(variable) attr_loc = location ? location : attribute_location(variable)
glUniform1i(attr_loc, value) glUniform1i(attr_loc, value)
end end
# send Float to {Shader}
#
# @param variable [String]
# @param value [Float]
# @param location [Integer]
# @return [void]
def uniform_float(variable, value, location = nil) def uniform_float(variable, value, location = nil)
attr_loc = location ? location : attribute_location(variable) attr_loc = location ? location : attribute_location(variable)
glUniform1f(attr_loc, value) glUniform1f(attr_loc, value)
end end
# send {Vector} (x, y, z) to {Shader}
#
# @param variable [String]
# @param value [Vector]
# @param location [Integer]
# @return [void]
def uniform_vec3(variable, value, location = nil) def uniform_vec3(variable, value, location = nil)
attr_loc = location ? location : attribute_location(variable) attr_loc = location ? location : attribute_location(variable)
glUniform3f(attr_loc, *value.to_a[0..2]) glUniform3f(attr_loc, *value.to_a[0..2])
end end
# send {Vector} to {Shader}
#
# @param variable [String]
# @param value [Vector]
# @param location [Integer]
# @return [void]
def uniform_vec4(variable, value, location = nil) def uniform_vec4(variable, value, location = nil)
attr_loc = location ? location : attribute_location(variable) attr_loc = location ? location : attribute_location(variable)

View File

@@ -1,25 +1,61 @@
module CyberarmEngine module CyberarmEngine
class Vector class Vector
##
# Creates a up vector
#
# Vector.new(0, 1, 0)
#
# @return [CyberarmEngine::Vector]
def self.up def self.up
Vector.new(0, 1, 0) Vector.new(0, 1, 0)
end end
##
# Creates a down vector
#
# Vector.new(0, -1, 0)
#
# @return [CyberarmEngine::Vector]
def self.down def self.down
Vector.new(0, -1, 0) Vector.new(0, -1, 0)
end end
##
# Creates a left vector
#
# Vector.new(-1, 0, 0)
#
# @return [CyberarmEngine::Vector]
def self.left def self.left
Vector.new(-1, 0, 0) Vector.new(-1, 0, 0)
end end
##
# Creates a right vector
#
# Vector.new(1, 0, 0)
#
# @return [CyberarmEngine::Vector]
def self.right def self.right
Vector.new(1, 0, 0) Vector.new(1, 0, 0)
end end
##
# Creates a forward vector
#
# Vector.new(0, 0, 1)
#
# @return [CyberarmEngine::Vector]
def self.forward def self.forward
Vector.new(0, 0, 1) Vector.new(0, 0, 1)
end end
##
# Creates a backward vector
#
# Vector.new(0, 0, -1)
#
# @return [CyberarmEngine::Vector]
def self.backward def self.backward
Vector.new(0, 0, -1) Vector.new(0, 0, -1)
end end
@@ -43,25 +79,30 @@ module CyberarmEngine
alias w weight alias w weight
alias w= weight= alias w= weight=
# @return [Boolean]
def ==(other) def ==(other)
if other.is_a?(Numeric) if other.is_a?(Numeric)
@x == other && @x == other &&
@y == other && @y == other &&
@z == other && @z == other &&
@weight == other @weight == other
else elsif other.is_a?(Vector)
@x == other.x && @x == other.x &&
@y == other.y && @y == other.y &&
@z == other.z && @z == other.z &&
@weight == other.weight @weight == other.weight
else
other == self
end end
end end
# Create a new vector using {x} and {y} values
# @return [CyberarmEngine::Vector]
def xy def xy
Vector.new(@x, @y) Vector.new(@x, @y)
end end
# Performs math operation, excluding @weight # Performs math operation, excluding {weight}
private def operator(function, other) private def operator(function, other)
if other.is_a?(Numeric) if other.is_a?(Numeric)
Vector.new( Vector.new(
@@ -78,22 +119,26 @@ module CyberarmEngine
end end
end end
# Adds Vector and Numberic or Vector and Vector, excluding @weight # Adds Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def +(other) def +(other)
operator("+", other) operator("+", other)
end end
# Subtracts Vector and Numberic or Vector and Vector, excluding @weight # Subtracts Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def -(other) def -(other)
operator("-", other) operator("-", other)
end end
# Multiplies Vector and Numberic or Vector and Vector, excluding @weight # Multiplies Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def *(other) def *(other)
operator("*", other) operator("*", other)
end end
# Divides Vector and Numberic or Vector and Vector, excluding @weight # Divides Vector and Numeric or Vector and Vector, excluding {weight}
# @return [CyberarmEngine::Vector]
def /(other) def /(other)
# Duplicated to protect from DivideByZero # Duplicated to protect from DivideByZero
if other.is_a?(Numeric) if other.is_a?(Numeric)
@@ -111,6 +156,8 @@ module CyberarmEngine
end end
end end
# dot product of {Vector}
# @return [Integer|Float]
def dot(other) def dot(other)
product = 0 product = 0
@@ -124,6 +171,8 @@ module CyberarmEngine
return product return product
end end
# cross product of {Vector}
# @return [CyberarmEngine::Vector]
def cross(other) def cross(other)
a = self.to_a a = self.to_a
b = other.to_a b = other.to_a
@@ -136,24 +185,40 @@ module CyberarmEngine
end end
# returns degrees # returns degrees
# @return [Float]
def angle(other) def angle(other)
Math.acos( self.normalized.dot(other.normalized) ) * 180 / Math::PI Math.acos( self.normalized.dot(other.normalized) ) * 180 / Math::PI
end end
# returns magnitude of Vector, ignoring #weight # returns magnitude of Vector, ignoring #weight
# @return [Float]
def magnitude def magnitude
Math.sqrt((@x * @x) + (@y * @y) + (@z * @z)) Math.sqrt((@x * @x) + (@y * @y) + (@z * @z))
end end
##
# returns normalized {Vector}
#
# @example
# CyberarmEngine::Vector.new(50, 21.2, 45).normalized
# # => <CyberarmEngine::Vector:0x001 @x=0.7089... @y=0.3005... @z=0.6380... @weight=0>
#
# @return [CyberarmEngine::Vector]
def normalized def normalized
mag = magnitude mag = magnitude
self / Vector.new(mag, mag, mag) self / Vector.new(mag, mag, mag)
end end
def direction
# returns a direction {Vector}
#
# z is pitch # z is pitch
#
# y is yaw # y is yaw
#
# x is roll # x is roll
# @return [CyberarmEngine::Vector]
def direction
_x = -Math.sin(@y.degrees_to_radians) * Math.cos(@z.degrees_to_radians) _x = -Math.sin(@y.degrees_to_radians) * Math.cos(@z.degrees_to_radians)
_y = Math.sin(@z.degrees_to_radians) _y = Math.sin(@z.degrees_to_radians)
_z = Math.cos(@y.degrees_to_radians) * Math.cos(@z.degrees_to_radians) _z = Math.cos(@y.degrees_to_radians) * Math.cos(@z.degrees_to_radians)
@@ -161,41 +226,63 @@ module CyberarmEngine
Vector.new(_x, _y, _z) Vector.new(_x, _y, _z)
end end
# returns an inverse {Vector}
# @return [CyberarmEngine::Vector]
def inverse def inverse
Vector.new(1.0 / @x, 1.0 / @y, 1.0 / @z) Vector.new(1.0 / @x, 1.0 / @y, 1.0 / @z)
end end
# Adds up values of {x}, {y}, and {z}
# @return [Integer|Float]
def sum def sum
@x + @y + @z @x + @y + @z
end end
##
# Linear interpolation: smoothly transition between two {Vector}
#
# CyberarmEngine::Vector.new(100, 100, 100).lerp( CyberarmEngine::Vector.new(0, 0, 0), 0.75 )
# # => <CyberarmEngine::Vector:0x0001 @x=75.0, @y=75.0, @z=75.0, @weight=0>
#
# @param other [CyberarmEngine::Vector | Integer | Float] value to subtract from
# @param factor [Float] how complete transition to _other_ is, in range [0.0..1.0]
# @return [CyberarmEngine::Vector]
def lerp(other, factor) def lerp(other, factor)
(self - other) * factor.clamp(0.0, 1.0) (self - other) * factor.clamp(0.0, 1.0)
end end
# 2D distance using X and Y # 2D distance using X and Y
# @return [Float]
def distance(other) def distance(other)
Math.sqrt((@x-other.x)**2 + (@y-other.y)**2) Math.sqrt((@x-other.x)**2 + (@y-other.y)**2)
end end
# 2D distance using X and Z # 2D distance using X and Z
# @return [Float]
def gl_distance2d(other) def gl_distance2d(other)
Math.sqrt((@x-other.x)**2 + (@z-other.z)**2) Math.sqrt((@x-other.x)**2 + (@z-other.z)**2)
end end
# 3D distance using X, Y, and Z # 3D distance using X, Y, and Z
# @return [Float]
def distance3d(other) def distance3d(other)
Math.sqrt((@x-other.x)**2 + (@y-other.y)**2 + (@z-other.z)**2) Math.sqrt((@x-other.x)**2 + (@y-other.y)**2 + (@z-other.z)**2)
end end
# Converts {Vector} to Array
# @return [Array]
def to_a def to_a
[@x, @y, @z, @weight] [@x, @y, @z, @weight]
end end
# Converts {Vector} to String
# @return [String]
def to_s def to_s
"X: #{@x}, Y: #{@y}, Z: #{@z}, Weight: #{@weight}" "X: #{@x}, Y: #{@y}, Z: #{@z}, Weight: #{@weight}"
end end
# Converts {Vector} to Hash
# @return [Hash]
def to_h def to_h
{x: @x, y: @y, z: @z, weight: @weight} {x: @x, y: @y, z: @z, weight: @weight}
end end

View File

@@ -1,4 +1,4 @@
module CyberarmEngine module CyberarmEngine
NAME = "InDev" NAME = "InDev"
VERSION = "0.13.0" VERSION = "0.13.1"
end end