/**************************************************************************/ /* godot_view.mm */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #import "godot_view.h" #import "display_layer.h" #import "display_server_ios.h" #import "godot_view_renderer.h" #include "core/config/project_settings.h" #include "core/os/keyboard.h" #include "core/string/ustring.h" #import static const int max_touches = 32; static const float earth_gravity = 9.80665; @interface GodotView () { UITouch *godot_touches[max_touches]; } @property(assign, nonatomic) BOOL isActive; // CADisplayLink available on 3.1+ synchronizes the animation timer & drawing with the refresh rate of the display, only supports animation intervals of 1/60 1/30 & 1/15 @property(strong, nonatomic) CADisplayLink *displayLink; // An animation timer that, when animation is started, will periodically call -drawView at the given rate. // Only used if CADisplayLink is not @property(strong, nonatomic) NSTimer *animationTimer; @property(strong, nonatomic) CALayer *renderingLayer; @property(strong, nonatomic) CMMotionManager *motionManager; @end @implementation GodotView - (CALayer *)initializeRenderingForDriver:(NSString *)driverName { if (self.renderingLayer) { return self.renderingLayer; } CALayer *layer; if ([driverName isEqualToString:@"vulkan"]) { #if defined(TARGET_OS_SIMULATOR) && TARGET_OS_SIMULATOR if (@available(iOS 13, *)) { layer = [GodotMetalLayer layer]; } else { return nil; } #else layer = [GodotMetalLayer layer]; #endif } else if ([driverName isEqualToString:@"opengl3"]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // OpenGL is deprecated in iOS 12.0 layer = [GodotOpenGLLayer layer]; #pragma clang diagnostic pop } else { return nil; } layer.frame = self.bounds; layer.contentsScale = self.contentScaleFactor; [self.layer addSublayer:layer]; self.renderingLayer = layer; [layer initializeDisplayLayer]; return self.renderingLayer; } - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { [self godot_commonInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self godot_commonInit]; } return self; } - (void)dealloc { [self stopRendering]; self.renderer = nil; self.delegate = nil; if (self.renderingLayer) { [self.renderingLayer removeFromSuperlayer]; self.renderingLayer = nil; } if (self.motionManager) { [self.motionManager stopDeviceMotionUpdates]; self.motionManager = nil; } if (self.displayLink) { [self.displayLink invalidate]; self.displayLink = nil; } if (self.animationTimer) { [self.animationTimer invalidate]; self.animationTimer = nil; } } - (void)godot_commonInit { self.contentScaleFactor = [UIScreen mainScreen].scale; [self initTouches]; self.multipleTouchEnabled = YES; // Configure and start accelerometer if (!self.motionManager) { self.motionManager = [[CMMotionManager alloc] init]; if (self.motionManager.deviceMotionAvailable) { self.motionManager.deviceMotionUpdateInterval = 1.0 / 70.0; [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXMagneticNorthZVertical]; } else { self.motionManager = nil; } } } - (void)system_theme_changed { DisplayServerIOS *ds = (DisplayServerIOS *)DisplayServer::get_singleton(); if (ds) { ds->emit_system_theme_changed(); } } - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { if (@available(iOS 13.0, *)) { [super traitCollectionDidChange:previousTraitCollection]; if ([UITraitCollection currentTraitCollection].userInterfaceStyle != previousTraitCollection.userInterfaceStyle) { [self system_theme_changed]; } } } - (void)stopRendering { if (!self.isActive) { return; } self.isActive = NO; print_verbose("Stop animation!"); if (self.useCADisplayLink) { [self.displayLink invalidate]; self.displayLink = nil; } else { [self.animationTimer invalidate]; self.animationTimer = nil; } [self clearTouches]; } - (void)startRendering { if (self.isActive) { return; } self.isActive = YES; print_verbose("Start animation!"); if (self.useCADisplayLink) { self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(drawView)]; if (GLOBAL_GET("display/window/ios/allow_high_refresh_rate")) { self.displayLink.preferredFramesPerSecond = 120; } else { self.displayLink.preferredFramesPerSecond = 60; } // Setup DisplayLink in main thread [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } else { self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60) target:self selector:@selector(drawView) userInfo:nil repeats:YES]; } } - (void)drawView { if (!self.isActive) { print_verbose("Draw view not active!"); return; } if (self.useCADisplayLink) { // Pause the CADisplayLink to avoid recursion [self.displayLink setPaused:YES]; // Process all input events while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, TRUE) == kCFRunLoopRunHandledSource) { // Continue. } // We are good to go, resume the CADisplayLink [self.displayLink setPaused:NO]; } [self.renderingLayer startRenderDisplayLayer]; if (!self.renderer) { return; } if ([self.renderer setupView:self]) { return; } if (self.delegate) { BOOL delegateFinishedSetup = [self.delegate godotViewFinishedSetup:self]; if (!delegateFinishedSetup) { return; } } [self handleMotion]; [self.renderer renderOnView:self]; [self.renderingLayer stopRenderDisplayLayer]; } - (BOOL)canRender { if (self.useCADisplayLink) { return self.displayLink != nil; } else { return self.animationTimer != nil; } } - (void)setRenderingInterval:(NSTimeInterval)renderingInterval { _renderingInterval = renderingInterval; if (self.canRender) { [self stopRendering]; [self startRendering]; } } - (void)layoutSubviews { if (self.renderingLayer) { self.renderingLayer.frame = self.bounds; [self.renderingLayer layoutDisplayLayer]; if (DisplayServerIOS::get_singleton()) { DisplayServerIOS::get_singleton()->resize_window(self.bounds.size); } } [super layoutSubviews]; } // MARK: - Input // MARK: Touches - (void)initTouches { for (int i = 0; i < max_touches; i++) { godot_touches[i] = nullptr; } } - (int)getTouchIDForTouch:(UITouch *)p_touch { int first = -1; for (int i = 0; i < max_touches; i++) { if (first == -1 && godot_touches[i] == nullptr) { first = i; continue; } if (godot_touches[i] == p_touch) { return i; } } if (first != -1) { godot_touches[first] = p_touch; return first; } return -1; } - (int)removeTouch:(UITouch *)p_touch { int remaining = 0; for (int i = 0; i < max_touches; i++) { if (godot_touches[i] == nullptr) { continue; } if (godot_touches[i] == p_touch) { godot_touches[i] = nullptr; } else { ++remaining; } } return remaining; } - (void)clearTouches { for (int i = 0; i < max_touches; i++) { godot_touches[i] = nullptr; } } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { int tid = [self getTouchIDForTouch:touch]; ERR_FAIL_COND(tid == -1); CGPoint touchPoint = [touch locationInView:self]; DisplayServerIOS::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, true, touch.tapCount > 1); } } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { int tid = [self getTouchIDForTouch:touch]; ERR_FAIL_COND(tid == -1); CGPoint touchPoint = [touch locationInView:self]; CGPoint prev_point = [touch previousLocationInView:self]; CGFloat alt = [touch altitudeAngle]; CGVector azim = [touch azimuthUnitVectorInView:self]; DisplayServerIOS::get_singleton()->touch_drag(tid, prev_point.x * self.contentScaleFactor, prev_point.y * self.contentScaleFactor, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, [touch force] / [touch maximumPossibleForce], Vector2(azim.dx, azim.dy) * Math::cos(alt)); } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { int tid = [self getTouchIDForTouch:touch]; ERR_FAIL_COND(tid == -1); [self removeTouch:touch]; CGPoint touchPoint = [touch locationInView:self]; DisplayServerIOS::get_singleton()->touch_press(tid, touchPoint.x * self.contentScaleFactor, touchPoint.y * self.contentScaleFactor, false, false); } } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { for (UITouch *touch in touches) { int tid = [self getTouchIDForTouch:touch]; ERR_FAIL_COND(tid == -1); DisplayServerIOS::get_singleton()->touches_canceled(tid); } [self clearTouches]; } // MARK: Motion - (void)handleMotion { if (!self.motionManager) { return; } // Just using polling approach for now, we can set this up so it sends // data to us in intervals, might be better. See Apple reference pages // for more details: // https://developer.apple.com/reference/coremotion/cmmotionmanager?language=objc // Apple splits our accelerometer date into a gravity and user movement // component. We add them back together. CMAcceleration gravity = self.motionManager.deviceMotion.gravity; CMAcceleration acceleration = self.motionManager.deviceMotion.userAcceleration; // To be consistent with Android we convert the unit of measurement from g (Earth's gravity) // to m/s^2. gravity.x *= earth_gravity; gravity.y *= earth_gravity; gravity.z *= earth_gravity; acceleration.x *= earth_gravity; acceleration.y *= earth_gravity; acceleration.z *= earth_gravity; ///@TODO We don't seem to be getting data here, is my device broken or /// is this code incorrect? CMMagneticField magnetic = self.motionManager.deviceMotion.magneticField.field; ///@TODO we can access rotationRate as a CMRotationRate variable ///(processed date) or CMGyroData (raw data), have to see what works /// best CMRotationRate rotation = self.motionManager.deviceMotion.rotationRate; // Adjust for screen orientation. // [[UIDevice currentDevice] orientation] changes even if we've fixed // our orientation which is not a good thing when you're trying to get // your user to move the screen in all directions and want consistent // output ///@TODO Using [[UIApplication sharedApplication] statusBarOrientation] /// is a bit of a hack. Godot obviously knows the orientation so maybe /// we // can use that instead? (note that left and right seem swapped) UIInterfaceOrientation interfaceOrientation = UIInterfaceOrientationUnknown; if (@available(iOS 13, *)) { interfaceOrientation = [UIApplication sharedApplication].delegate.window.windowScene.interfaceOrientation; #if !defined(TARGET_OS_SIMULATOR) || !TARGET_OS_SIMULATOR } else { interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation]; #endif } switch (interfaceOrientation) { case UIInterfaceOrientationLandscapeLeft: { DisplayServerIOS::get_singleton()->update_gravity(-gravity.y, gravity.x, gravity.z); DisplayServerIOS::get_singleton()->update_accelerometer(-(acceleration.y + gravity.y), (acceleration.x + gravity.x), acceleration.z + gravity.z); DisplayServerIOS::get_singleton()->update_magnetometer(-magnetic.y, magnetic.x, magnetic.z); DisplayServerIOS::get_singleton()->update_gyroscope(-rotation.y, rotation.x, rotation.z); } break; case UIInterfaceOrientationLandscapeRight: { DisplayServerIOS::get_singleton()->update_gravity(gravity.y, -gravity.x, gravity.z); DisplayServerIOS::get_singleton()->update_accelerometer((acceleration.y + gravity.y), -(acceleration.x + gravity.x), acceleration.z + gravity.z); DisplayServerIOS::get_singleton()->update_magnetometer(magnetic.y, -magnetic.x, magnetic.z); DisplayServerIOS::get_singleton()->update_gyroscope(rotation.y, -rotation.x, rotation.z); } break; case UIInterfaceOrientationPortraitUpsideDown: { DisplayServerIOS::get_singleton()->update_gravity(-gravity.x, gravity.y, gravity.z); DisplayServerIOS::get_singleton()->update_accelerometer(-(acceleration.x + gravity.x), (acceleration.y + gravity.y), acceleration.z + gravity.z); DisplayServerIOS::get_singleton()->update_magnetometer(-magnetic.x, magnetic.y, magnetic.z); DisplayServerIOS::get_singleton()->update_gyroscope(-rotation.x, rotation.y, rotation.z); } break; default: { // assume portrait DisplayServerIOS::get_singleton()->update_gravity(gravity.x, gravity.y, gravity.z); DisplayServerIOS::get_singleton()->update_accelerometer(acceleration.x + gravity.x, acceleration.y + gravity.y, acceleration.z + gravity.z); DisplayServerIOS::get_singleton()->update_magnetometer(magnetic.x, magnetic.y, magnetic.z); DisplayServerIOS::get_singleton()->update_gyroscope(rotation.x, rotation.y, rotation.z); } break; } } @end