Ring Menu
Version: 2.0.0 "updated on 20/09/2010"
By: Trebor777
Introduction
A nice customizable Ring Menu
that you can where you want! It's not a scene!
but an actual "object" like a window.
Features
1.8.1 -> 2.0.0
.Demos Updated
.Better and Simpler code structure, Menu_Options now Inherits from Sprite, less redondant code
.More comments, explaning what does what
.Move Command: @menu.move(x,y)
.Doesn't block other graphics update
.Introduction of Refresh Delay: Menu is only update every "delay" frame, by default delay is 1, so it's updated every frame.
.For options, name is now independant, and doesn't not setup the icon->
in VX: if the icon parameter is a number, it'll look in the iconset, but if it's a string, it'll look in the icon folder, as it would happen in XP
in XP: the icon parameter is a string, to look for a file in the icon folder.
1.8->1.8.1
Fixed: You can now used Up/Down Keys when it's a vertical ring menu
Screenshots
-> From Castlevania :
Demo
Basic demo for VX 400KB Updated to 2.0.0
Basic demo for XP 262KB Updated to 2.0.0
Look at the Scene_Demo script
Script
SCRIPT FOR VX:
[rgss]#===============================================================================
<span style="color:#000080; font-style:italic;">=begin
<span style="color:#000080; font-style:italic;">RING MENU advanced.
<span style="color:#000080; font-style:italic;">by Trebor777
<span style="color:#000080; font-style:italic;">v2.0.0
<span style="color:#000080; font-style:italic;">20/09/2010
<span style="color:#000080; font-style:italic;">An independant, class free, Ring menu, easily usable, with a lot of options.
<span style="color:#000080; font-style:italic;">=end
#===============================================================================
class Array
# rotate n item(s) to the left ( first becomes last) as a new array
def rotate_l(n=1)
a = self.clone
n.times{a.push(a.shift)}
a
end
# rotate n item(s) to the right ( last becomes first) as a new array
def rotate_r(n=1)
a = self.clone
n.times{a.unshift(a.pop)}
a
end
# rotate n item(s) to the left
def rotate_l!(n=1)
n.times{self.push(self.shift)}
self
end
# rotate n item(s) to the right
def rotate_r!(n=1)
n.times{self.unshift(self.pop)}
self
end
# Do the sum of all the items. (they need to be all of the same type)
def sum
inject( nil ) { |sum,x| sum ? sum+x : x if x.respond_to? "+" }
end
end
#===============================================================================
module Cache
def self.icon(filename)
load_bitmap("Graphics/Icons/",filename)
end
end
#===============================================================================
class Ring_Menu
RESO = 4 # Default Number of Frames between 2 position
PLAY_SE = true # Play Sound Effect when scrolling ?
SELECTED_ANGLE= Math::PI/2.0 # 90°CW to set the "0" at the bottom
#---------------------------------
attr_reader
ptions, :h_radius, :v_radius, :center, :zoom, :visible,
pacity, :zoom_max, :resolution
attr_accessor :active, :index, :refresh_delay
alias :reso :resolution
#---------------------------------
def initialize(h_rad=10,v_rad=10,center=[0,0],options=[])
@options=options # List of Commands
@v_radius=v_rad # Vertical Radius, how wide is the menu on the Y-axis
@h_radius=h_rad # Horizontal Radius, how wide is the menu on the X-axis
@center = center # Position of the center, in the screen
@index=0 # Cursor Position, used to get the current selected option
@resolution = RESO # Number of frames betwen 2 commands, the larger the smoother, but more time needed
@refresh_delay = 1 # By default, refresh the menu every frame (1).
@zoom_max = 1.4 # Max Scale factor, for the perspective Effect (only for Horizontal Menu)
@opacity = 255 # General Opacity, gets applied to every command
@visible = true # Show or Hide the Menu.
@active = true # Enable / Disable the Menu.
@zoom = true # Activate the Perspective Effect
@angle = vertical? ? 0 : SELECTED_ANGLE # Where is the first command on the circle. Not recommended to change
process
end
# Attributes
#---------------------------------
def x
@center[0]
end
#---------------------------------
def y
@center[1]
end
#---------------------------------
def center=(value) # Placing is moving at a specific distance
delta_x = value[0] - @center[0]
delta_y = value[1] - @center[1]
move(delta_x, delta_y)
end
#---------------------------------
def x=(*args)
self.center=[ args.first, @center[1] ]
end
#---------------------------------
def y=(*args)
self.center=[ @center[0], args.first ]
end
#---------------------------------
def options=(a=[])
@options=a
@index=0 # reset the Index position
process
end
#---------------------------------
def visible=(value)
@visible = value
@options.each{|opt| opt.visible=value unless opt.nil?}
end
#---------------------------------
def opacity=(value)
value = [[255,value].min, 0].max # Set value Range between 0..255
@opacity = value
@options.each{|opt| opt.opacity=value unless opt.nil?}
end
#---------------------------------
def h_radius=(value)
@h_radius = value
@angle = vertical? ? 0 : SELECTED_ANGLE
process
end
#---------------------------------
def v_radius=(value)
@v_radius = value
process
end
#---------------------------------
def zoom=(value)
@zoom = value
update_draw
end
#---------------------------------
def resolution=(v)
@resolution = v
process
end
alias :reso= :resolution=
#---------------------------------
def zoom_max=(value)
@zoom_max= value
process
end
# HELPERS Method ==============
def vertical? # Is the menu vertical (meaning that the horizontal radius is zero)
@h_radius == 0
end
#---------------------------------
def check_z_selected
self.selected.z =1000 if vertical? # Set the selected option to the top
end
#--------------------------------
def move(d_x, d_y) # Moves center from d_x , d_y, every option will be moved accordingly
@center[0] += d_x
@center[1] += d_y
end
# ======================
#---------------------------------
# Update, Called Every Frame
#---------------------------------
def update
return if !self.active or !self.visible
return unless Graphics.frame_count % @refresh_delay == 0
unless @rotating
start_rotate if Input.repeat?(vertical? ? Input::UP : Input::LEFT)
start_rotate(1) if Input.repeat?(vertical? ? Input::DOWN : Input::RIGHT)
end
if @rotating
if @counter+1==@resolution
unless self.selected.disabled
@rotating = false
self.selected.selected = true
check_z_selected
else#if self.selected.disabled
@index += @way*2-1
@index %= @options.size
end
end
if @way.zero?
@coord_set.rotate_l!
@zoom_set.rotate_l!
elsif @way==1
@coord_set.rotate_r!
@zoom_set.rotate_r!
end
@counter+=1
@counter%=@resolution
end
update_draw
end
#---------------------------------
# Process: Create/recreate according to the menu attributes, the position/zoom values of the options, and store them into arrays.
#---------------------------------
def process
# return if the menu is empty.
return if @options.empty?
# Get the maximum size for the arrays.
max_data=@options.size*@resolution
# Divide a circle(2*Pi) by the numbers of options.
@base_angle = Math::PI*2.0/max_data
@zoom_step = (@zoom_max-0.5)/(max_data/2.0)
# set an array having precalculated pair of coordinates so we don't have to
# get process them for each rotation(we simply switch the options coords)
@coord_set= Array.new(max_data,0)
# and for zoom values as well
@zoom_set = Array.new(max_data,0)
max_data.times do |i|
angle = i*@base_angle+@angle
x = (Math::cos(angle)*@h_radius) # Calculates relative position, as if center was in 0,0
y = (Math::sin(angle)*@v_radius)
@coord_set= [x,y]
@zoom_set= i<max_data/2.0 ? @zoom_max-@zoom_step*i : @zoom_set[max_data/2.0-i-max_data/2.0-1]
end
val = (@index % max_data)+1+((@resolution-4)/2.0).round
@coord_set.rotate_r!(val)
@zoom_set.rotate_r!(val)
@old_center = @center
@old_hr = @h_radius
@old_vr = @v_radius
update_draw
self.selected.selected = true
check_z_selected
end
#--------------------------------------
# Draw the options according to the current index
#--------------------------------------
def update_draw
@coord_set.each_index do |index|
i = (index / @resolution.to_f).round
# next iteration if the current option is not a RMenu_Option
next if !@options.is_a?(RMenu_Option)
# Set the Option coords
@options.coord = @coord_set[index]
@options.x += @center[0] # Place options relatively to the center.
@options.y += @center[1]
# Set the option's Z-index to its y.
@options.z = @coord_set[index][1]
@options.zoom = @zoom ? @zoom_set[index] : 1.0
end
@old_zoom = @zoom
check_z_selected unless @rotating
end
#---------------------------------
# Rotate the menu options
#---------------------------------
def start_rotate(way=0)# 0 = left, 1 = right
self.selected.selected = false
@rotating = true
@counter = 0
@way = way
@index += @way*2-1
@index %= @options.size
Sound.play_cursor if PLAY_SE
end
#---------------------------------
# Return the current selected option
#---------------------------------
def selected
@options[@index % @options.size]
end
#---------------------------------
# Dispose the menu (and all the options)
#---------------------------------
def dispose
@options.each{|opt| opt.dispose}
end
#--------------------------------------------------------------------------
# * Hide -> disable and hides the menu
#--------------------------------------------------------------------------
def hide
self.visible = false
self.active = false
end
#--------------------------------------------------------------------------
# * Show -> enable and show the menu
#--------------------------------------------------------------------------
def show
self.visible = true
self.active = true
end
end
#===============================================================================
# Menu option class, Inherits from the Sprite class, to ease management.
#===============================================================================
class RMenu_Option < Sprite
attr_reader :name, :icon, :selected, :disabled
#--------------------------------------------------------------------------
def initialize(name="",icon=0,viewport=nil,&block)
super(viewport)
@name=name
@icon=icon
@block=block
@disabled = false
self.selected = false
if icon > 0 and !icon.is_a? String
self.bitmap = Bitmap.new(24,24)
self.draw_icon(@icon,0,0,true)
elsif icon.is_a? String
self.bitmap = Cache.icon(icon)
end
self.ox = self.width/2
self.oy = self.height/2
end
#--------------------------------------------------------------------------
# * Draw Icon
# icon_index : Icon number
# x : draw spot x-coordinate
# y : draw spot y-coordinate
# enabled : Enabled flag. When false, draw semi-transparently.
#--------------------------------------------------------------------------
def draw_icon(icon_index, x, y, enabled = true)
bitmap = Cache.system("Iconset")
rect = Rect.new(icon_index % 16 * 24, icon_index / 16 * 24, 24, 24)
self.bitmap.blt(x, y, bitmap, rect, enabled ? 255 : 128)
end
#--------------------------------------------------------------------------
def call # Execute the Code passed as an argument when the option was created
@block.call unless @block.nil?
end
#--------------------------------------------------------------------------
def selected=(v)
if v != @selected
self.tone.gray = (v==true ? 0 : 255)
@selected = v==true
end
end
#--------------------------------------------------------------------------
def disabled=(v)
@disabled= v==true # v == true makes sure that v is a boolean, meaning that if v isn't a boolean, @disabled will be set to false
end
#--------------------------------------------------------------------------
def coord=(c = [0,0])
self.x = c[0]
self.y = c[1]
end
#--------------------------------------------------------------------------
def zoom=value
self.zoom_x = value
self.zoom_y = value
end
#--------------------------------------------------------------------------
def dispose
@coord=nil
@block=nil
super
end
end
[/rgss]
SCRIPT FOR XP:
[rgss]#===============================================================================
<span style="color:#000080; font-style:italic;">=begin
<span style="color:#000080; font-style:italic;">RING MENU advanced. XP VERSION
<span style="color:#000080; font-style:italic;">by Trebor777
<span style="color:#000080; font-style:italic;">v2.0.0
<span style="color:#000080; font-style:italic;">20/09/2010
<span style="color:#000080; font-style:italic;">An independant, class free, Ring menu, easily usable, with a lot of options.
<span style="color:#000080; font-style:italic;">=end
#===============================================================================
class Array
# rotate n item(s) to the left ( first becomes last) as a new array
def rotate_l(n=1)
a = self.clone
n.times{a.push(a.shift)}
a
end
# rotate n item(s) to the right ( last becomes first) as a new array
def rotate_r(n=1)
a = self.clone
n.times{a.unshift(a.pop)}
a
end
# rotate n item(s) to the left
def rotate_l!(n=1)
n.times{self.push(self.shift)}
self
end
# rotate n item(s) to the right
def rotate_r!(n=1)
n.times{self.unshift(self.pop)}
self
end
# Do the sum of all the items. (they need to be all of the same type)
def sum
inject( nil ) { |sum,x| sum ? sum+x : x if x.respond_to? "+" }
end
end
#===============================================================================
module Sound
#--------------------------------------------------------------------------
# * Play Sound Effect
# se : sound effect to be played
#--------------------------------------------------------------------------
def self.se_play(se)
if se != nil and se.name != ""
Audio.se_play("Audio/SE/" + se.name, se.volume, se.pitch)
end
end
def self.play_cursor
se_play($data_system.decision_se) unless $data_system.nil?
end
end
#===============================================================================
class Ring_Menu
RESO = 4 # Default Number of Frames between 2 position
PLAY_SE = true # Play Sound Effect when scrolling ?
SELECTED_ANGLE= Math::PI/2.0 # 90°CW to set the "0" at the bottom
#---------------------------------
attr_reader
ptions, :h_radius, :v_radius, :center, :zoom, :visible,
pacity, :zoom_max, :resolution
attr_accessor :active, :index, :refresh_delay
alias :reso :resolution
#---------------------------------
def initialize(h_rad=10,v_rad=10,center=[0,0],options=[])
@options=options # List of Commands
@v_radius=v_rad # Vertical Radius, how wide is the menu on the Y-axis
@h_radius=h_rad # Horizontal Radius, how wide is the menu on the X-axis
@center = center # Position of the center, in the screen
@index=0 # Cursor Position, used to get the current selected option
@resolution = RESO # Number of frames betwen 2 commands, the larger the smoother, but more time needed
@refresh_delay = 1 # By default, refresh the menu every frame (1).
@zoom_max = 1.4 # Max Scale factor, for the perspective Effect (only for Horizontal Menu)
@opacity = 255 # General Opacity, gets applied to every command
@visible = true # Show or Hide the Menu.
@active = true # Enable / Disable the Menu.
@zoom = true # Activate the Perspective Effect
@angle = vertical? ? 0 : SELECTED_ANGLE # Where is the first command on the circle. Not recommended to change
process
end
# Attributes
#---------------------------------
def x
@center[0]
end
#---------------------------------
def y
@center[1]
end
#---------------------------------
def center=(value) # Placing is moving at a specific distance
delta_x = value[0] - @center[0]
delta_y = value[1] - @center[1]
move(delta_x, delta_y)
end
#---------------------------------
def x=(*args)
self.center=[ args.first, @center[1] ]
end
#---------------------------------
def y=(*args)
self.center=[ @center[0], args.first ]
end
#---------------------------------
def options=(a=[])
@options=a
@index=0 # reset the Index position
process
end
#---------------------------------
def visible=(value)
@visible = value
@options.each{|opt| opt.visible=value unless opt.nil?}
end
#---------------------------------
def opacity=(value)
value = [[255,value].min, 0].max # Set value Range between 0..255
@opacity = value
@options.each{|opt| opt.opacity=value unless opt.nil?}
end
#---------------------------------
def h_radius=(value)
@h_radius = value
@angle = vertical? ? 0 : SELECTED_ANGLE
process
end
#---------------------------------
def v_radius=(value)
@v_radius = value
process
end
#---------------------------------
def zoom=(value)
@zoom = value
update_draw
end
#---------------------------------
def resolution=(v)
@resolution = v
process
end
alias :reso= :resolution=
#---------------------------------
def zoom_max=(value)
@zoom_max= value
process
end
# HELPERS Method ==============
def vertical? # Is the menu vertical (meaning that the horizontal radius is zero)
@h_radius == 0
end
#---------------------------------
def check_z_selected
self.selected.z =1000 if vertical? # Set the selected option to the top
end
#--------------------------------
def move(d_x, d_y) # Moves center from d_x , d_y, every option will be moved accordingly
@center[0] += d_x
@center[1] += d_y
end
# ======================
#---------------------------------
# Update, Called Every Frame
#---------------------------------
def update
return if !self.active or !self.visible
return unless Graphics.frame_count % @refresh_delay == 0
unless @rotating
start_rotate if Input.repeat?(vertical? ? Input::UP : Input::LEFT)
start_rotate(1) if Input.repeat?(vertical? ? Input::DOWN : Input::RIGHT)
end
if @rotating
if @counter+1==@resolution
unless self.selected.disabled
@rotating = false
self.selected.selected = true
check_z_selected
else#if self.selected.disabled
@index += @way*2-1
@index %= @options.size
end
end
if @way.zero?
@coord_set.rotate_l!
@zoom_set.rotate_l!
elsif @way==1
@coord_set.rotate_r!
@zoom_set.rotate_r!
end
@counter+=1
@counter%=@resolution
end
update_draw
end
#---------------------------------
# Process: Create/recreate according to the menu attributes, the position/zoom values of the options, and store them into arrays.
#---------------------------------
def process
# return if the menu is empty.
return if @options.empty?
# Get the maximum size for the arrays.
max_data=@options.size*@resolution
# Divide a circle(2*Pi) by the numbers of options.
@base_angle = Math::PI*2.0/max_data
@zoom_step = (@zoom_max-0.5)/(max_data/2.0)
# set an array having precalculated pair of coordinates so we don't have to
# get process them for each rotation(we simply switch the options coords)
@coord_set= Array.new(max_data,0)
# and for zoom values as well
@zoom_set = Array.new(max_data,0)
max_data.times do |i|
angle = i*@base_angle+@angle
x = (Math::cos(angle)*@h_radius) # Calculates relative position, as if center was in 0,0
y = (Math::sin(angle)*@v_radius)
@coord_set= [x,y]
@zoom_set= i<max_data/2.0 ? @zoom_max-@zoom_step*i : @zoom_set[max_data/2.0-i-max_data/2.0-1]
end
val = (@index % max_data)+1+((@resolution-4)/2.0).round
@coord_set.rotate_r!(val)
@zoom_set.rotate_r!(val)
@old_center = @center
@old_hr = @h_radius
@old_vr = @v_radius
update_draw
self.selected.selected = true
check_z_selected
end
#--------------------------------------
# Draw the options according to the current index
#--------------------------------------
def update_draw
@coord_set.each_index do |index|
i = (index / @resolution.to_f).round
# next iteration if the current option is not a RMenu_Option
next if !@options.is_a?(RMenu_Option)
# Set the Option coords
@options.coord = @coord_set[index]
@options.x += @center[0] # Place options relatively to the center.
@options.y += @center[1]
# Set the option's Z-index to its y.
@options.z = @coord_set[index][1]
@options.zoom = @zoom ? @zoom_set[index] : 1.0
end
@old_zoom = @zoom
check_z_selected unless @rotating
end
#---------------------------------
# Rotate the menu options
#---------------------------------
def start_rotate(way=0)# 0 = left, 1 = right
self.selected.selected = false
@rotating = true
@counter = 0
@way = way
@index += @way*2-1
@index %= @options.size
Sound.play_cursor if PLAY_SE
end
#---------------------------------
# Return the current selected option
#---------------------------------
def selected
@options[@index % @options.size]
end
#---------------------------------
# Dispose the menu (and all the options)
#---------------------------------
def dispose
@options.each{|opt| opt.dispose}
end
#--------------------------------------------------------------------------
# * Hide -> disable and hides the menu
#--------------------------------------------------------------------------
def hide
self.visible = false
self.active = false
end
#--------------------------------------------------------------------------
# * Show -> enable and show the menu
#--------------------------------------------------------------------------
def show
self.visible = true
self.active = true
end
end
#===============================================================================
# Menu option class, Inherits from the Sprite class, to ease management.
#===============================================================================
class RMenu_Option < Sprite
attr_reader :name, :icon_name, :selected, :disabled
#--------------------------------------------------------------------------
def initialize(name="",icon="",viewport=nil,&block)
super(viewport)
@name=name
@icon_name=icon
@block=block
@disabled = false
self.selected = false
self.bitmap = RPG::Cache.icon(@icon_name)
self.ox = self.bitmap.width/2
self.oy = self.bitmap.height/2
end
#--------------------------------------------------------------------------
def call # Execute the Code passed as an argument when the option was created
@block.call unless @block.nil?
end
#--------------------------------------------------------------------------
def selected=(v)
if v != @selected
self.tone.gray = (v==true ? 0 : 255)
@selected = v==true
end
end
#--------------------------------------------------------------------------
def disabled=(v)
@disabled= v==true # v == true makes sure that v is a boolean, meaning that if v isn't a boolean, @disabled will be set to false
end
#--------------------------------------------------------------------------
def coord=(c = [0,0])
self.x = c[0]
self.y = c[1]
end
#--------------------------------------------------------------------------
def zoom=value
self.zoom_x = value
self.zoom_y = value
end
#--------------------------------------------------------------------------
def dispose
@coord=nil
@block=nil
super
end
end
[/rgss]
Instructions
Very easy to use, only a few things to keep in mind!
->Paste the script above main!
->Now to create a ring menu:
Step 1
- you need to create the options like this =>
FOR VX ONLY : If icon_id is a string , it will look for an image named with icon_id in a folder called "Icons" in the Graphics folder. Else, if it's a number >0, it'll take the icon from the icon set
FOR XP only: Because XP doesn't have an icon set, the icon feature only works with the name, the xp script doesn't care about the Icon id, and will only look for a valid icon name
If you don't set up any graphics, it won't display anything. Anyway you can access the option bitmap
so you can do whatever you want with it.
The block is optional as well
if not needed, don't write anything after the parenthesis.
Step 2
You place those options inside a temporary array, this array will be used when creating the menu:
Step 3
Create the menu!
-> by default h_radius and v_radius = 10, x & y = 0 and opt_array is an empty array.
Yes that means you can add/remove options later on!
That's for the basic Creation!
-> The menu needs to be updated! Like any normal graphical object in a scene.
menu.update
No need to use code stuff for arrow keys, it's already done in the update method.
-> AND to be disposed as well
menu.dispose
To get the current selected option : menu.selected
Therefore: to execute the optional block given at the option creation, -> menu.selected.call
Like a window, the menu is only active or visible when you want it: menu.active/visible = true/false,
The zoom attribute: if true ( menu.zoom = true ), it'll activate the perspective effect, -> basically the options 'in front' of us are bigger than the options in the back.
By default the "max size" is 40% bigger than the original file (zoom_x/y = 1.4)
But you can change that max value if required. For example -> menu.zoom_max = 1.0
If you need to change the fluidity of the menu scrolling: you need to use -> menu.reso= value
by default this value is 4, it means it takes 4 frames to go from one option to another.
If you intend to change the "fluidity", it's important that you do it before anything else.
You can play with the opacity, as well like sprites.
FAQ
"I've noticed some constants in the RingMenu class, what are they? Can I use them?"
3 Constants:
- RESO : the default "fluidity"(number of frames) used to move from one option ot another, you can change it, if you don't want to do it for several menu that use the same value
- PLAY_SE : if false, the menu won't make any noise when being scrolled.
- SELECTED_ANGLE : I suggest you don't touch it. The function -> Because a 0° angle is on the right on the trigonometric circle, I need to "rotate" that circle in order to have the 0 , in front of me, which is at the bottom. It is used when the menu is "horizontal", to have the selected option, in front of us, instead of the right of the ring. The angle is in Radian.
When I press Up/Down on a vertical ring menu, nothing happens, Why ?
-> Check that the horizontal radius is 0, if not, Use the left/right keys!
Compatibility
I've added Methods to the cache module, to the array Class, the sprite Class as well
As long as no other classes are called like mines, it should be fine. ( too lazy to put mine into a module. )
Author's Notes
I did this script for the 1st time in 12/2007 on XP
and updated for my needs with time.
It's used in Castlevania as well. And I thought I should release it, cause it's a very convenient thing to use!
Terms and Conditions
Everywhere you want, just credit me
Version: 2.0.0 "updated on 20/09/2010"
By: Trebor777
Introduction
A nice customizable Ring Menu
but an actual "object" like a window.
Features
- Specify the radius for the Vertical and Horizontal dimension.
If Horizontal is 0, you have a vertical riing menu.
- Pass a block of code to each options!
- Icons can be from the icon set or an independant file.
- "Perspective effect" if "zoom" activated.
- Smooth "scrolling" between options
you can even specify how smooth you want it!
- Easy access to the "selected" option, and possibilty to disable options as well
( they will be skipped when scrolling )
- Non-static => you can move the menu around and change its attributes after creation!
- Brings a few useful methods to the Array class
1.8.1 -> 2.0.0
.Demos Updated
.Better and Simpler code structure, Menu_Options now Inherits from Sprite, less redondant code
.More comments, explaning what does what
.Move Command: @menu.move(x,y)
.Doesn't block other graphics update
.Introduction of Refresh Delay: Menu is only update every "delay" frame, by default delay is 1, so it's updated every frame.
.For options, name is now independant, and doesn't not setup the icon->
in VX: if the icon parameter is a number, it'll look in the iconset, but if it's a string, it'll look in the icon folder, as it would happen in XP
in XP: the icon parameter is a string, to look for a file in the icon folder.
1.8->1.8.1
Fixed: You can now used Up/Down Keys when it's a vertical ring menu
Screenshots
-> From Castlevania :


Demo
Basic demo for VX 400KB Updated to 2.0.0
Basic demo for XP 262KB Updated to 2.0.0
Look at the Scene_Demo script
Script
SCRIPT FOR VX:
[rgss]#===============================================================================
<span style="color:#000080; font-style:italic;">=begin
<span style="color:#000080; font-style:italic;">RING MENU advanced.
<span style="color:#000080; font-style:italic;">by Trebor777
<span style="color:#000080; font-style:italic;">v2.0.0
<span style="color:#000080; font-style:italic;">20/09/2010
<span style="color:#000080; font-style:italic;">An independant, class free, Ring menu, easily usable, with a lot of options.
<span style="color:#000080; font-style:italic;">=end
#===============================================================================
class Array
# rotate n item(s) to the left ( first becomes last) as a new array
def rotate_l(n=1)
a = self.clone
n.times{a.push(a.shift)}
a
end
# rotate n item(s) to the right ( last becomes first) as a new array
def rotate_r(n=1)
a = self.clone
n.times{a.unshift(a.pop)}
a
end
# rotate n item(s) to the left
def rotate_l!(n=1)
n.times{self.push(self.shift)}
self
end
# rotate n item(s) to the right
def rotate_r!(n=1)
n.times{self.unshift(self.pop)}
self
end
# Do the sum of all the items. (they need to be all of the same type)
def sum
inject( nil ) { |sum,x| sum ? sum+x : x if x.respond_to? "+" }
end
end
#===============================================================================
module Cache
def self.icon(filename)
load_bitmap("Graphics/Icons/",filename)
end
end
#===============================================================================
class Ring_Menu
RESO = 4 # Default Number of Frames between 2 position
PLAY_SE = true # Play Sound Effect when scrolling ?
SELECTED_ANGLE= Math::PI/2.0 # 90°CW to set the "0" at the bottom
#---------------------------------
attr_reader
attr_accessor :active, :index, :refresh_delay
alias :reso :resolution
#---------------------------------
def initialize(h_rad=10,v_rad=10,center=[0,0],options=[])
@options=options # List of Commands
@v_radius=v_rad # Vertical Radius, how wide is the menu on the Y-axis
@h_radius=h_rad # Horizontal Radius, how wide is the menu on the X-axis
@center = center # Position of the center, in the screen
@index=0 # Cursor Position, used to get the current selected option
@resolution = RESO # Number of frames betwen 2 commands, the larger the smoother, but more time needed
@refresh_delay = 1 # By default, refresh the menu every frame (1).
@zoom_max = 1.4 # Max Scale factor, for the perspective Effect (only for Horizontal Menu)
@opacity = 255 # General Opacity, gets applied to every command
@visible = true # Show or Hide the Menu.
@active = true # Enable / Disable the Menu.
@zoom = true # Activate the Perspective Effect
@angle = vertical? ? 0 : SELECTED_ANGLE # Where is the first command on the circle. Not recommended to change
process
end
# Attributes
#---------------------------------
def x
@center[0]
end
#---------------------------------
def y
@center[1]
end
#---------------------------------
def center=(value) # Placing is moving at a specific distance
delta_x = value[0] - @center[0]
delta_y = value[1] - @center[1]
move(delta_x, delta_y)
end
#---------------------------------
def x=(*args)
self.center=[ args.first, @center[1] ]
end
#---------------------------------
def y=(*args)
self.center=[ @center[0], args.first ]
end
#---------------------------------
def options=(a=[])
@options=a
@index=0 # reset the Index position
process
end
#---------------------------------
def visible=(value)
@visible = value
@options.each{|opt| opt.visible=value unless opt.nil?}
end
#---------------------------------
def opacity=(value)
value = [[255,value].min, 0].max # Set value Range between 0..255
@opacity = value
@options.each{|opt| opt.opacity=value unless opt.nil?}
end
#---------------------------------
def h_radius=(value)
@h_radius = value
@angle = vertical? ? 0 : SELECTED_ANGLE
process
end
#---------------------------------
def v_radius=(value)
@v_radius = value
process
end
#---------------------------------
def zoom=(value)
@zoom = value
update_draw
end
#---------------------------------
def resolution=(v)
@resolution = v
process
end
alias :reso= :resolution=
#---------------------------------
def zoom_max=(value)
@zoom_max= value
process
end
# HELPERS Method ==============
def vertical? # Is the menu vertical (meaning that the horizontal radius is zero)
@h_radius == 0
end
#---------------------------------
def check_z_selected
self.selected.z =1000 if vertical? # Set the selected option to the top
end
#--------------------------------
def move(d_x, d_y) # Moves center from d_x , d_y, every option will be moved accordingly
@center[0] += d_x
@center[1] += d_y
end
# ======================
#---------------------------------
# Update, Called Every Frame
#---------------------------------
def update
return if !self.active or !self.visible
return unless Graphics.frame_count % @refresh_delay == 0
unless @rotating
start_rotate if Input.repeat?(vertical? ? Input::UP : Input::LEFT)
start_rotate(1) if Input.repeat?(vertical? ? Input::DOWN : Input::RIGHT)
end
if @rotating
if @counter+1==@resolution
unless self.selected.disabled
@rotating = false
self.selected.selected = true
check_z_selected
else#if self.selected.disabled
@index += @way*2-1
@index %= @options.size
end
end
if @way.zero?
@coord_set.rotate_l!
@zoom_set.rotate_l!
elsif @way==1
@coord_set.rotate_r!
@zoom_set.rotate_r!
end
@counter+=1
@counter%=@resolution
end
update_draw
end
#---------------------------------
# Process: Create/recreate according to the menu attributes, the position/zoom values of the options, and store them into arrays.
#---------------------------------
def process
# return if the menu is empty.
return if @options.empty?
# Get the maximum size for the arrays.
max_data=@options.size*@resolution
# Divide a circle(2*Pi) by the numbers of options.
@base_angle = Math::PI*2.0/max_data
@zoom_step = (@zoom_max-0.5)/(max_data/2.0)
# set an array having precalculated pair of coordinates so we don't have to
# get process them for each rotation(we simply switch the options coords)
@coord_set= Array.new(max_data,0)
# and for zoom values as well
@zoom_set = Array.new(max_data,0)
max_data.times do |i|
angle = i*@base_angle+@angle
x = (Math::cos(angle)*@h_radius) # Calculates relative position, as if center was in 0,0
y = (Math::sin(angle)*@v_radius)
@coord_set= [x,y]
@zoom_set= i<max_data/2.0 ? @zoom_max-@zoom_step*i : @zoom_set[max_data/2.0-i-max_data/2.0-1]
end
val = (@index % max_data)+1+((@resolution-4)/2.0).round
@coord_set.rotate_r!(val)
@zoom_set.rotate_r!(val)
@old_center = @center
@old_hr = @h_radius
@old_vr = @v_radius
update_draw
self.selected.selected = true
check_z_selected
end
#--------------------------------------
# Draw the options according to the current index
#--------------------------------------
def update_draw
@coord_set.each_index do |index|
i = (index / @resolution.to_f).round
# next iteration if the current option is not a RMenu_Option
next if !@options.is_a?(RMenu_Option)
# Set the Option coords
@options.coord = @coord_set[index]
@options.x += @center[0] # Place options relatively to the center.
@options.y += @center[1]
# Set the option's Z-index to its y.
@options.z = @coord_set[index][1]
@options.zoom = @zoom ? @zoom_set[index] : 1.0
end
@old_zoom = @zoom
check_z_selected unless @rotating
end
#---------------------------------
# Rotate the menu options
#---------------------------------
def start_rotate(way=0)# 0 = left, 1 = right
self.selected.selected = false
@rotating = true
@counter = 0
@way = way
@index += @way*2-1
@index %= @options.size
Sound.play_cursor if PLAY_SE
end
#---------------------------------
# Return the current selected option
#---------------------------------
def selected
@options[@index % @options.size]
end
#---------------------------------
# Dispose the menu (and all the options)
#---------------------------------
def dispose
@options.each{|opt| opt.dispose}
end
#--------------------------------------------------------------------------
# * Hide -> disable and hides the menu
#--------------------------------------------------------------------------
def hide
self.visible = false
self.active = false
end
#--------------------------------------------------------------------------
# * Show -> enable and show the menu
#--------------------------------------------------------------------------
def show
self.visible = true
self.active = true
end
end
#===============================================================================
# Menu option class, Inherits from the Sprite class, to ease management.
#===============================================================================
class RMenu_Option < Sprite
attr_reader :name, :icon, :selected, :disabled
#--------------------------------------------------------------------------
def initialize(name="",icon=0,viewport=nil,&block)
super(viewport)
@name=name
@icon=icon
@block=block
@disabled = false
self.selected = false
if icon > 0 and !icon.is_a? String
self.bitmap = Bitmap.new(24,24)
self.draw_icon(@icon,0,0,true)
elsif icon.is_a? String
self.bitmap = Cache.icon(icon)
end
self.ox = self.width/2
self.oy = self.height/2
end
#--------------------------------------------------------------------------
# * Draw Icon
# icon_index : Icon number
# x : draw spot x-coordinate
# y : draw spot y-coordinate
# enabled : Enabled flag. When false, draw semi-transparently.
#--------------------------------------------------------------------------
def draw_icon(icon_index, x, y, enabled = true)
bitmap = Cache.system("Iconset")
rect = Rect.new(icon_index % 16 * 24, icon_index / 16 * 24, 24, 24)
self.bitmap.blt(x, y, bitmap, rect, enabled ? 255 : 128)
end
#--------------------------------------------------------------------------
def call # Execute the Code passed as an argument when the option was created
@block.call unless @block.nil?
end
#--------------------------------------------------------------------------
def selected=(v)
if v != @selected
self.tone.gray = (v==true ? 0 : 255)
@selected = v==true
end
end
#--------------------------------------------------------------------------
def disabled=(v)
@disabled= v==true # v == true makes sure that v is a boolean, meaning that if v isn't a boolean, @disabled will be set to false
end
#--------------------------------------------------------------------------
def coord=(c = [0,0])
self.x = c[0]
self.y = c[1]
end
#--------------------------------------------------------------------------
def zoom=value
self.zoom_x = value
self.zoom_y = value
end
#--------------------------------------------------------------------------
def dispose
@coord=nil
@block=nil
super
end
end
[/rgss]
SCRIPT FOR XP:
[rgss]#===============================================================================
<span style="color:#000080; font-style:italic;">=begin
<span style="color:#000080; font-style:italic;">RING MENU advanced. XP VERSION
<span style="color:#000080; font-style:italic;">by Trebor777
<span style="color:#000080; font-style:italic;">v2.0.0
<span style="color:#000080; font-style:italic;">20/09/2010
<span style="color:#000080; font-style:italic;">An independant, class free, Ring menu, easily usable, with a lot of options.
<span style="color:#000080; font-style:italic;">=end
#===============================================================================
class Array
# rotate n item(s) to the left ( first becomes last) as a new array
def rotate_l(n=1)
a = self.clone
n.times{a.push(a.shift)}
a
end
# rotate n item(s) to the right ( last becomes first) as a new array
def rotate_r(n=1)
a = self.clone
n.times{a.unshift(a.pop)}
a
end
# rotate n item(s) to the left
def rotate_l!(n=1)
n.times{self.push(self.shift)}
self
end
# rotate n item(s) to the right
def rotate_r!(n=1)
n.times{self.unshift(self.pop)}
self
end
# Do the sum of all the items. (they need to be all of the same type)
def sum
inject( nil ) { |sum,x| sum ? sum+x : x if x.respond_to? "+" }
end
end
#===============================================================================
module Sound
#--------------------------------------------------------------------------
# * Play Sound Effect
# se : sound effect to be played
#--------------------------------------------------------------------------
def self.se_play(se)
if se != nil and se.name != ""
Audio.se_play("Audio/SE/" + se.name, se.volume, se.pitch)
end
end
def self.play_cursor
se_play($data_system.decision_se) unless $data_system.nil?
end
end
#===============================================================================
class Ring_Menu
RESO = 4 # Default Number of Frames between 2 position
PLAY_SE = true # Play Sound Effect when scrolling ?
SELECTED_ANGLE= Math::PI/2.0 # 90°CW to set the "0" at the bottom
#---------------------------------
attr_reader
attr_accessor :active, :index, :refresh_delay
alias :reso :resolution
#---------------------------------
def initialize(h_rad=10,v_rad=10,center=[0,0],options=[])
@options=options # List of Commands
@v_radius=v_rad # Vertical Radius, how wide is the menu on the Y-axis
@h_radius=h_rad # Horizontal Radius, how wide is the menu on the X-axis
@center = center # Position of the center, in the screen
@index=0 # Cursor Position, used to get the current selected option
@resolution = RESO # Number of frames betwen 2 commands, the larger the smoother, but more time needed
@refresh_delay = 1 # By default, refresh the menu every frame (1).
@zoom_max = 1.4 # Max Scale factor, for the perspective Effect (only for Horizontal Menu)
@opacity = 255 # General Opacity, gets applied to every command
@visible = true # Show or Hide the Menu.
@active = true # Enable / Disable the Menu.
@zoom = true # Activate the Perspective Effect
@angle = vertical? ? 0 : SELECTED_ANGLE # Where is the first command on the circle. Not recommended to change
process
end
# Attributes
#---------------------------------
def x
@center[0]
end
#---------------------------------
def y
@center[1]
end
#---------------------------------
def center=(value) # Placing is moving at a specific distance
delta_x = value[0] - @center[0]
delta_y = value[1] - @center[1]
move(delta_x, delta_y)
end
#---------------------------------
def x=(*args)
self.center=[ args.first, @center[1] ]
end
#---------------------------------
def y=(*args)
self.center=[ @center[0], args.first ]
end
#---------------------------------
def options=(a=[])
@options=a
@index=0 # reset the Index position
process
end
#---------------------------------
def visible=(value)
@visible = value
@options.each{|opt| opt.visible=value unless opt.nil?}
end
#---------------------------------
def opacity=(value)
value = [[255,value].min, 0].max # Set value Range between 0..255
@opacity = value
@options.each{|opt| opt.opacity=value unless opt.nil?}
end
#---------------------------------
def h_radius=(value)
@h_radius = value
@angle = vertical? ? 0 : SELECTED_ANGLE
process
end
#---------------------------------
def v_radius=(value)
@v_radius = value
process
end
#---------------------------------
def zoom=(value)
@zoom = value
update_draw
end
#---------------------------------
def resolution=(v)
@resolution = v
process
end
alias :reso= :resolution=
#---------------------------------
def zoom_max=(value)
@zoom_max= value
process
end
# HELPERS Method ==============
def vertical? # Is the menu vertical (meaning that the horizontal radius is zero)
@h_radius == 0
end
#---------------------------------
def check_z_selected
self.selected.z =1000 if vertical? # Set the selected option to the top
end
#--------------------------------
def move(d_x, d_y) # Moves center from d_x , d_y, every option will be moved accordingly
@center[0] += d_x
@center[1] += d_y
end
# ======================
#---------------------------------
# Update, Called Every Frame
#---------------------------------
def update
return if !self.active or !self.visible
return unless Graphics.frame_count % @refresh_delay == 0
unless @rotating
start_rotate if Input.repeat?(vertical? ? Input::UP : Input::LEFT)
start_rotate(1) if Input.repeat?(vertical? ? Input::DOWN : Input::RIGHT)
end
if @rotating
if @counter+1==@resolution
unless self.selected.disabled
@rotating = false
self.selected.selected = true
check_z_selected
else#if self.selected.disabled
@index += @way*2-1
@index %= @options.size
end
end
if @way.zero?
@coord_set.rotate_l!
@zoom_set.rotate_l!
elsif @way==1
@coord_set.rotate_r!
@zoom_set.rotate_r!
end
@counter+=1
@counter%=@resolution
end
update_draw
end
#---------------------------------
# Process: Create/recreate according to the menu attributes, the position/zoom values of the options, and store them into arrays.
#---------------------------------
def process
# return if the menu is empty.
return if @options.empty?
# Get the maximum size for the arrays.
max_data=@options.size*@resolution
# Divide a circle(2*Pi) by the numbers of options.
@base_angle = Math::PI*2.0/max_data
@zoom_step = (@zoom_max-0.5)/(max_data/2.0)
# set an array having precalculated pair of coordinates so we don't have to
# get process them for each rotation(we simply switch the options coords)
@coord_set= Array.new(max_data,0)
# and for zoom values as well
@zoom_set = Array.new(max_data,0)
max_data.times do |i|
angle = i*@base_angle+@angle
x = (Math::cos(angle)*@h_radius) # Calculates relative position, as if center was in 0,0
y = (Math::sin(angle)*@v_radius)
@coord_set= [x,y]
@zoom_set= i<max_data/2.0 ? @zoom_max-@zoom_step*i : @zoom_set[max_data/2.0-i-max_data/2.0-1]
end
val = (@index % max_data)+1+((@resolution-4)/2.0).round
@coord_set.rotate_r!(val)
@zoom_set.rotate_r!(val)
@old_center = @center
@old_hr = @h_radius
@old_vr = @v_radius
update_draw
self.selected.selected = true
check_z_selected
end
#--------------------------------------
# Draw the options according to the current index
#--------------------------------------
def update_draw
@coord_set.each_index do |index|
i = (index / @resolution.to_f).round
# next iteration if the current option is not a RMenu_Option
next if !@options.is_a?(RMenu_Option)
# Set the Option coords
@options.coord = @coord_set[index]
@options.x += @center[0] # Place options relatively to the center.
@options.y += @center[1]
# Set the option's Z-index to its y.
@options.z = @coord_set[index][1]
@options.zoom = @zoom ? @zoom_set[index] : 1.0
end
@old_zoom = @zoom
check_z_selected unless @rotating
end
#---------------------------------
# Rotate the menu options
#---------------------------------
def start_rotate(way=0)# 0 = left, 1 = right
self.selected.selected = false
@rotating = true
@counter = 0
@way = way
@index += @way*2-1
@index %= @options.size
Sound.play_cursor if PLAY_SE
end
#---------------------------------
# Return the current selected option
#---------------------------------
def selected
@options[@index % @options.size]
end
#---------------------------------
# Dispose the menu (and all the options)
#---------------------------------
def dispose
@options.each{|opt| opt.dispose}
end
#--------------------------------------------------------------------------
# * Hide -> disable and hides the menu
#--------------------------------------------------------------------------
def hide
self.visible = false
self.active = false
end
#--------------------------------------------------------------------------
# * Show -> enable and show the menu
#--------------------------------------------------------------------------
def show
self.visible = true
self.active = true
end
end
#===============================================================================
# Menu option class, Inherits from the Sprite class, to ease management.
#===============================================================================
class RMenu_Option < Sprite
attr_reader :name, :icon_name, :selected, :disabled
#--------------------------------------------------------------------------
def initialize(name="",icon="",viewport=nil,&block)
super(viewport)
@name=name
@icon_name=icon
@block=block
@disabled = false
self.selected = false
self.bitmap = RPG::Cache.icon(@icon_name)
self.ox = self.bitmap.width/2
self.oy = self.bitmap.height/2
end
#--------------------------------------------------------------------------
def call # Execute the Code passed as an argument when the option was created
@block.call unless @block.nil?
end
#--------------------------------------------------------------------------
def selected=(v)
if v != @selected
self.tone.gray = (v==true ? 0 : 255)
@selected = v==true
end
end
#--------------------------------------------------------------------------
def disabled=(v)
@disabled= v==true # v == true makes sure that v is a boolean, meaning that if v isn't a boolean, @disabled will be set to false
end
#--------------------------------------------------------------------------
def coord=(c = [0,0])
self.x = c[0]
self.y = c[1]
end
#--------------------------------------------------------------------------
def zoom=value
self.zoom_x = value
self.zoom_y = value
end
#--------------------------------------------------------------------------
def dispose
@coord=nil
@block=nil
super
end
end
[/rgss]
Instructions
Very easy to use, only a few things to keep in mind!
->Paste the script above main!
->Now to create a ring menu:
Step 1
- you need to create the options like this =>
Code:
RMenu_Option.new("Name", [icon_id, viewport]) { optional_block }
FOR XP only: Because XP doesn't have an icon set, the icon feature only works with the name, the xp script doesn't care about the Icon id, and will only look for a valid icon name
If you don't set up any graphics, it won't display anything. Anyway you can access the option bitmap
The block is optional as well
Step 2
You place those options inside a temporary array, this array will be used when creating the menu:
Code:
opt = [
RMenu_Option.new("SelData"){ $scene = Scene_Data.new },
RMenu_Option.new("CopData"){ $scene = Scene_Copy_Data.new },
RMenu_Option.new("DelData"){ $scene = Scene_Del_Data.new }
]
Step 3
Create the menu!
Code:
Ring_Menu.new( [h_radius, v_radius, [x,y], opt_array] )
Yes that means you can add/remove options later on!
That's for the basic Creation!
-> The menu needs to be updated! Like any normal graphical object in a scene.
No need to use code stuff for arrow keys, it's already done in the update method.
-> AND to be disposed as well
To get the current selected option : menu.selected
Therefore: to execute the optional block given at the option creation, -> menu.selected.call
Like a window, the menu is only active or visible when you want it: menu.active/visible = true/false,
The zoom attribute: if true ( menu.zoom = true ), it'll activate the perspective effect, -> basically the options 'in front' of us are bigger than the options in the back.
By default the "max size" is 40% bigger than the original file (zoom_x/y = 1.4)
But you can change that max value if required. For example -> menu.zoom_max = 1.0
If you need to change the fluidity of the menu scrolling: you need to use -> menu.reso= value
by default this value is 4, it means it takes 4 frames to go from one option to another.
If you intend to change the "fluidity", it's important that you do it before anything else.
You can play with the opacity, as well like sprites.
FAQ
"I've noticed some constants in the RingMenu class, what are they? Can I use them?"
3 Constants:
- RESO : the default "fluidity"(number of frames) used to move from one option ot another, you can change it, if you don't want to do it for several menu that use the same value
- PLAY_SE : if false, the menu won't make any noise when being scrolled.
- SELECTED_ANGLE : I suggest you don't touch it. The function -> Because a 0° angle is on the right on the trigonometric circle, I need to "rotate" that circle in order to have the 0 , in front of me, which is at the bottom. It is used when the menu is "horizontal", to have the selected option, in front of us, instead of the right of the ring. The angle is in Radian.
When I press Up/Down on a vertical ring menu, nothing happens, Why ?
-> Check that the horizontal radius is 0, if not, Use the left/right keys!
Compatibility
I've added Methods to the cache module, to the array Class, the sprite Class as well
As long as no other classes are called like mines, it should be fine. ( too lazy to put mine into a module. )
Author's Notes
I did this script for the 1st time in 12/2007 on XP
It's used in Castlevania as well. And I thought I should release it, cause it's a very convenient thing to use!
Terms and Conditions
Everywhere you want, just credit me