Hello! I - lead developer cocos2d-objc. We are now in the process of porting to Swift. I plan to cover the development process, tell about architectural solutions, etc. So far, the project is still on the proof-of-a-concept stage, so today I will only talk about a small trick, which, I believe, made our mathematical library a little better. If interested - please under the cat.
Before you start rewriting the Swift engine, the need for a modern library for mathematical needs became apparent. The engine is initially x-platform in mind, so we could not use CG * types, and CoreGraphics API is not enough Swifty for us. The existing solutions did not satisfy us, so we decided to write our bicycle, while adhering to a certain austerity.
We have limited ourselves to a modest set of types:
Vector2f, Vector3f, Vector4f, Matrix3f, Matrix4f, Rect . We firmly decided that we want to completely eliminate ARC overhead and definitely want to have
SIMD support (at least on
Darwin platforms, for
Glibc so far the algorithms are written manually until simd is available publicly), for this reason we had to abandon generic and tie the entire library on the type of
Float , without the support of
Double .
')
Ok, I realized that you are not interested, this is the introduction. What is the article about?
At some point, we realized that an API that works with angles (such as methods for creating a rotation matrix, etc.) needs to be improved. The problem that seemed important to us is to eliminate the need for users to look at the documentation for what they expect to receive the method: radians or degrees.
Initially, we wanted to make aliases on Float like:
typealias Radians = Float; typealias Degrees = Float;
It is clear that this does not save much, because it is still possible to pass the value in the method to the wrong value.
We also considered the creation of the functions rad () and deg (), which would return the desired value. Variants of Int and Float Extensions, as well as their Type Literals
45.degrees 180.radians
We did not like, because allowed to do:
180.radians.radians
As a result, it was decided to create a separate structure for the Angle type:
/// A floating point value that represents an angle public struct Angle { /// The value of the angle in degrees public let degrees: Float /// The value of the angle in radians public var radians: Float { return degrees * Float.pi / 180.0 } /// Creates an instance using the value in radians @inline(__always) public init(radians val: Float) { degrees = val / Float.pi * 180.0 } /// Creates an instance using the value in degrees @inline(__always) public init(degrees val: Float) { degrees = val } @inline(__always) internal init(_ val: Float) { degrees = val } }
What is good? Now we can tell the user that the method expects angle as a parameter, and he doesn’t need to worry in what form: in radians or degrees.
If he conveys the structure of Angle, then he is sure that everything will work correctly.
We have defined all standard operators for working with Angle (the same as for scalar values, only Angle / Angle returns Float instead of Angle, and Angle * Angle does not exist at all)
We also decided to leave the extension for Int:
extension Int { /// Returns the integer value as an angle in degrees public var degrees: Angle { return Angle(degrees: Float(self)) } }
Thus, we operate with our angles in degrees, without losing accuracy where it is not necessary and convert them to radians only when necessary (usually, with finite calculations).
To further ensure accuracy, we defined sin and cos for Angle like this:
@inline(__always) internal func sinf(_ a: Angle) -> Float { return __sinpif(a.degrees / 180.0) } @inline(__always) internal func cosf(_ a: Angle) -> Float { return __cospif(a.degrees / 180.0) }
True, for Glibc I had to write the usual implementation of these functions, since there are no features of increased accuracy.
And finally: the use of unicode in code is always a controversial topic. Personally, I don’t welcome it at all. Initially, we added the following operator for fun:
/// The degree operator constructs an `Angle` from the specified floating point value in degrees /// /// - remark: /// * Degree operator is the unicode symbol U+00B0 DEGREE SIGN /// * macOS shortcut is ⌘+⇧+8 @inline(__always) public postfix func °(lhs: Float) -> Angle { return Angle(degrees: lhs) } /// Constructs an `Angle` from the specified `Int` value in degrees @inline(__always) public postfix func °(lhs: Int) -> Angle { return Angle(degrees: Float(lhs)) }
And we defined our constants like this:
// MARK: Constants public static let zero = 0° public static let pi_6 = 30° public static let pi_4 = 45° public static let pi_3 = 60° public static let pi_2 = 90° public static let pi2_3 = 120° public static let pi = 180° public static let pi3_2 = 270° public static let pi2 = 360°
As a result, I found myself using this operator in the code of the engine itself, this adds readability, but do not abuse it - not everyone remembers the shortcuts and sometimes it
infuriates .
The article was too big for a fairly trivial decision, but I hope you were interested. Applying this technique, we solved the problem, when you need to drive values back and forth and read the documentation for what values are expected.
→ You can follow the porting of the engine
here .
→
Link to math lib.