I’ve been a fan of DragonRuby since its release. I’ve been programming for a while, but I’m not familiar with game programming. I’ve been creating small projects for myself to explore and learn. This time I decided to make a simple analog clock with labels for the ordinals and lines for the second, minute, and hour hand.
The entry point to a DragonRuby project is a tick
method in a main.rb
file. I decided
to take a object-oriented this time with a Clock
class. I’ve seen examples where there
are .new
calls the tick
method, but I think it would be better to create one instance
outside of tick
and pass it arguments. Each tick I rendered all of the parts.
My basic structure was this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class Clock def tick(args) render_ordinals(args) render_second_hand(args) render_minute_hand(args) render_hour_hand(args) end def render_ordinals(args) end def render_second_hand(args) end def render_minute_hand(args) end def render_hour_hand(args) end end @clock = Clock.new def tick(args) @clock.tick(args) end |
I decided to work on the second hand first because I thought it would be tricky, but,
once I figured it out, the rest of the hands would be easy. I would need some constants
for doing the math. I wanted to put the clock in the middle. It would have been easier if
I changed the coordinate system to be in the middle (DragonRuby lets you do that), but I
purposely wanted to learn the default coordinate system that goes 0-1280 in the x
direction and 0-720 in the y
direction. I knew that I would need to convert degrees to
radians, so I put that constant in. Lastly, I wanted a radius to go off of. This was the
result:
1 2 3 4 5 6 7 | class Clock MID_X = 1280 / 2 MID_Y = 720 / 2 RADIAN = Math::PI / 180 RADIUS = 180 ... |
For my render_second_hand
method I decided I would need two parameters: the args
so
that I could output a line and seconds
which would be the current seconds. I would need
to convert the seconds to degrees around the clock’s
circle (seconds * 360/60
), those degrees to radians (seconds * 360/60 * RADIAN
),
and trigonometric functions (sin
for y
and cos
for x
) to x and y coordinates. I
would also need to multiply by a fraction of my radius to get the line length
(RADIUS * 0.8
). Lastly, the trigonometric functions naturally go counterclockwise (I
would need to reverse that) and start in the wrong position for my clock (I would need to
add an offset). This was my result:
1 2 3 4 5 6 | def render_second_hand(args, seconds) seconds_x = RADIUS * 0.8 * Math.cos((seconds + 45) * 360 / 60 * RADIAN) + MID_X seconds_y = -RADIUS * 0.8 * Math.sin((seconds + 45) * 360 / 60 * RADIAN) + MID_Y args.outputs.lines << [MID_X, MID_Y, seconds_x, seconds_y] end |
With this worked out, the render_minute_hand
method was nearly identical:
1 2 3 4 5 6 | def render_minute_hand(args, minutes) minutes_x = RADIUS * 0.6 * Math.cos((minutes + 45) * 360 / 60 * RADIAN) + MID_X minutes_y = -RADIUS * 0.6 * Math.sin((minutes + 45) * 360 / 60 * RADIAN) + MID_Y args.outputs.lines << [MID_X, MID_Y, minutes_x, minutes_y] end |
And the render_hour_hand
:
1 2 3 4 5 6 | def render_hour_hand(args, hours) hours_x = RADIUS * 0.4 * Math.cos((hours + 45) * 360 / 12 * RADIAN) + MID_X hours_y = -RADIUS * 0.4 * Math.sin((hours + 45) * 360 / 12 * RADIAN) + MID_Y args.outputs.lines << [MID_X, MID_Y, hours_x, hours_y] end |
Lastly, I needed to render the numbers (ordinals) around the outside:
1 2 3 4 5 6 7 8 | def render_ordinals(args) (1..12).each do |ordinal| x = RADIUS * Math.cos((ordinal * 30 - 90) * RADIAN) + MID_X y = -RADIUS * Math.sin((ordinal * 30 - 90) * RADIAN) + MID_Y args.outputs.labels << { x: x, y: y, text: ordinal, size: 5, alignment: 1 } end end |
With all of this, my code was shaping up nicely:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | class Clock MID_X = 1280 / 2 MID_Y = 720 / 2 RADIAN = Math::PI / 180 RADIUS = 180 def tick(args) time = Time.now render_ordinals(args) render_second_hand(args, time.sec) render_minute_hand(args, time.min) render_hour_hand(args, time.hour) end def render_ordinals(args) (1..12).each do |ordinal| x = RADIUS * Math.cos((ordinal*30 - 90) * RADIAN) + MID_X y = -RADIUS * Math.sin((ordinal*30 - 90) * RADIAN) + MID_Y args.outputs.labels << { x: x, y: y, text: ordinal, size: 5, alignment: 1 } end end def render_second_hand(args, seconds) seconds_x = RADIUS * 0.8 * Math.cos((seconds + 45) * 360 / 60 * RADIAN) + MID_X seconds_y = -RADIUS * 0.8 * Math.sin((seconds + 45) * 360 / 60 * RADIAN) + MID_Y args.outputs.lines << [MID_X, MID_Y, seconds_x, seconds_y] end def render_minute_hand(args, minutes) minutes_x = RADIUS * 0.6 * Math.cos((minutes + 45) * 360 / 60 * RADIAN) + MID_X minutes_y = -RADIUS * 0.6 * Math.sin((minutes + 45) * 360 / 60 * RADIAN) + MID_Y args.outputs.lines << [MID_X, MID_Y, minutes_x, minutes_y] end def render_hour_hand(args, hours) hours_x = RADIUS * 0.4 * Math.cos((hours + 45) * 360 / 12 * RADIAN) + MID_X hours_y = -RADIUS * 0.4 * Math.sin((hours + 45) * 360 / 12 * RADIAN) + MID_Y args.outputs.lines << [MID_X, MID_Y, hours_x, hours_y] end end @clock = Clock.new def tick(args) @clock.tick(args) end |
There were two last things that bothered me. First was that my ordinals didn’t line up quite right, but labels are tricky and I didn’t care to worry about each one. Secondly, the motion was jerky and abrupt. This was because I was using integers for my time values. I decided to use fractional time in my final result and it worked better:
1 2 3 4 5 6 7 8 | def tick(args) time = Time.now render_ordinals(args) render_second_hand(args, time.sec + time.usec / 1000000) render_minute_hand(args, time.min + time.sec / 60) render_hour_hand(args, time.hour + time.min / 60) end |
Thanks for reading!